Token access, reuse and auto refresh within the SDK

寵の児 提交于 2019-12-23 04:17:11

问题


Coming from C# and other languages, and new to F# and trying to port an SDK Library that I built in an OO language. The SDK is responsible for retrieving access token first, setting on a static field and then setting a specific interval to continuosly refresh the token before it's expiry.

The token is set as static field on the Authentication class and is updated every time before it reaches expiry.

Then other actors within the SDK reach out to Authentication class, read it's static field token, place in their Authorization headers before calling the REST endpoints. All actors within the SDK reuse the same token throughout for each call until it's expired and a newer one is auto fetched.

That's the behavior, I'm still trying to wrap my head around several concepts but I believe in learning while doing it.

This F# library will be called from C# where it will be passed Credentials to begin with and then subsequently instantiating other classes/actors and calling their methods while passing params for each individual method. Those other actors are the one who will be using this stored token.

The gist is basically having two static fields within the Authentication and enable access to other actors while refreshing the one of the static fields i.e. Token.

public class Authentication 
{
     public static Token token;
     public static Credentials credentials;
     public static Token RequestToken(Credentials credentials)
     {
           Authentication.credentials = credentials // cache for subsequent use
           // http REST call to access token based on credentials/api keys etc.
           // Authentication.token = someAccessTokenObject; // cache result

     }

     public static Token AddTokenObserver(Credentials credentials) 
     {
            this.RequestToken(credentials);
            // set interval, like call RequestToken every 24 hrs
     }
}

public class Class1 
{
     public someReturnObject RequestService1(someParams) {
           // accesses Authentication.credentials
           // accesses Authentication.token
           // places in the authorization headers 
           // and calls the web service
     }
      // + several other methods that repeats the same pattern
}

public class Class2
{
     public someReturnObject RequestService2(someParams) {
           // accesses Authentication.credentials
           // accesses Authentication.token
           // places in the authorization headers 
           // and calls the web service
     }
     // + several other methods that repeats the same pattern
}

Use of SDK

// initialize SDK by passing credentials and enable auto refresh
Authentication.AddTokenObserver(someCredentials) // set token observer

Class1 c1 = new Class1();
c1.RequestService1(someObject1); // uses credentials & token from Authentication

Class c2 = new Class2();
c2.RequestService2(someObject2); // uses credentials & token from Authentication

My F# Attempt

type Credentials = {mutable clientId: string; mutable clientSecret: string;}
type Token = {mutable access_token: string; mutable refresh_token: string}

type Authentication =
    static member token = {access_token = ""; refresh_token = ""};
    static member credentials = {clientId = ""; clientSecret = "";}            
    new() = {}

    member this.RequestToken(credentials) =
        let data : byte[] = System.Text.Encoding.ASCII.GetBytes("");

        let host = "https://example.com";
        let url = sprintf "%s&client_id=%s&client_secret=%s" host credentials.clientId credentials.clientSecret

        let request = WebRequest.Create(url) :?> HttpWebRequest
        request.Method <- "POST"
        request.ContentType <- "application/x-www-form-urlencoded"
        request.Accept <- "application/json;charset=UTF-8"
        request.ContentLength <- (int64)data.Length

        use requestStream = request.GetRequestStream() 
        requestStream.Write(data, 0, (data.Length))
        requestStream.Flush()
        requestStream.Close()

        let response = request.GetResponse() :?> HttpWebResponse

        use reader = new StreamReader(response.GetResponseStream())
        let output = reader.ReadToEnd()

        printf "%A" response.StatusCode // if response.StatusCode = HttpStatusCode.OK throws an error

        Authentication.credentials.clientId <- credentials.clientId

        let t = JsonConvert.DeserializeObject<Token>(output)                            
        Authentication.token.access_token <- t.access_token
        Authentication.token.token_type <- t.token_type

        reader.Close()
        response.Close()
        request.Abort()

F# Test

    [<TestMethod>]    
    member this.TestCredentials() = 
        let credentials = {
            clientId = "some client id"; 
            clientSecret = "some client secret"; 
        }
        let authenticaiton = new Authentication()

        try
            authenticaiton.RequestToken(credentials) 
            printfn "%s, %s" credentials.clientId Authentication.credentials.clientId // Authentication.credentials.clientId is empty string
            Assert.IsTrue(credentials.clientId = Authentication.credentials.clientId) // fails
        with
            | :? WebException -> printfn "error";

Question

In the above unit test

Authentication.credentials.clientId is empty string
Assert fails

I couldn't access static members within my unit tests after I called the token service. There's something wrong with how I'm approaching this all together.

I need help in translating C# behavior in F# with the help of some F# code. I've built the Authentication class and have some problems in the implementation especially around static members and subsequently accessing them. Also I want to follow the rules of functional programming and learn how it's done in Functional World in F#. Please help me translate this behavior in F# code.


回答1:


The idiomatic functional approach to this problem, would be to first try to get rid of the global state.

There are several approaches to this, but the one that I think will work best would be to provide an AuthenticationContext which has the data your C# code keeps in global state, and make each call that migth renew the credentials, return its result together with a potentially updated authorization context.

Basically, given a method to make an API call with a token

type MakeApiCall<'Result> = Token -> 'Result

we want to create something like this:

type AuthenticatedCall<'Result> = AuthenticationContext -> 'Result * AuthenticationContext

You can then also let the context keep track of whether it needs renewal (e.g. by storing the timestamp when it was last renewed, storing the expiry date, or something else), and provide two functions

type NeedsRenewal = AuthenticationContext -> bool
type Renew = AuthenticationContext -> AuthenticationContext

Now, if you obtain the credentials with a function

type GetAccessToken = AuthenticationContext -> Token * AuthenticationContext

you can let the implementation of that method start by checking if the credentials need renewal, and if so renew them before returning.

So, a sample implementation might look like this:

type AuthenticationContext = {
    credentials : Credentials
    token : Token
    expiryDate : DateTimeOffset
}

let needsRenewal context =
    context.expiryDate > DateTimeOffset.UtcNow.AddMinutes(-5) // add some safety margin

let renew context =
    let token = getNewToken context.Credentials
    let expiryDate = DateTimeOffset.UtcNow.AddDays(1)
    { context with token = token, expiryDate = expiryDate }

let getAccessToken context =
    let context' =
        if needsRenewal context
        then renew context
        else context

    return context'.token, context'

let makeAuthenticatedCall context makeApicall =
    let token, context' = getAccessToken context

    let result = makeApiCall token

    result, context'

Now, if every time you make an API call you have access to the AuthenticationContext from the previous call, the infrastructure will take care of renewing the token for you.


You'll notice quickly that this will just push the problem to keeping track of the authentication context, and that you will have to pass that around a lot. For example, if you want to make two consecutive API calls, you'll do the following:

let context = getInitialContext ()

let resultA, context' = makeFirstCall context
let resultB, context'' = makeSecondCall context'

Wouldn't it be nice if we could build something that would keep track of the context for us, so we didn't have to pass it around?

It turns out there's a functional pattern for this situation.



来源:https://stackoverflow.com/questions/49186245/token-access-reuse-and-auto-refresh-within-the-sdk

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!