MMMothClient

Basic OAuth 2.0 client supporting OpenID as well.

OAuth Refresher

  1. The app (“client”) wants to get access to protected resources of the end-user and thus needs a permission from them in the form of an access token, a string that the app is going to show to the server storing those resources.

  2. In order to obtain the access token the app opens a web browser (in-app or external) and navigates it to the corresponding “authorization server”. It tells the server what kind of app it is (“client identifier”), what resources the talk is about (“scope”), and how the credentials should be returned back (“response type”).

  3. The end-user authenticates themselves on this page and confirms that they are OK with the app accessing the resources in question.

  4. The server responds back to the app by directing the browser to a “redirect URL” mentioned by the app earlier adding its response in “query” or “fragment” parts of this URL.

  5. The app then either directly finds the access token in this response URL (“implicit flow”) or uses a short-lived code from there to get the access token via a separate network call to the “token endpoint” (“authorization code flow”).

Note that the extra step about code seems like having no sense for a native app. This is because it was designed for web servers and similar clients allowing them to obtain access tokens without the end-user (and thus any malicious code running on their device) seeing them. Note also that the access tokens usually expire but might be refreshed using a “refresh token”. A quirk of the protocol is that this token, if available, can be obtained only via “authorization code flow”.

OpenID Refresher

OpenID is essentially an OAuth flow where the resource being accessed is some basic information about the user. Implementation-wise the “scope” of such a flow includes “openid” and a more fancy JWT ID Token is returned in addition to (or instead of) the usual access token. It’s an official replacement for ad-hoc OAuth flows used earlier to authenticate users of one service by asking them via OAuth for access to their email address or basic details let say on another service.

Usage

TLDR: call start*() and watch the state to become .authorized helping with authorization UI when needed.

Nothing happens when the client is initialized, i.e. it stays at .idle.

Once start() and friends are called the client checks the storage for the existing credentials corresponding to the passed scope and response types:

  • If credentials were found and are not expired or can be refreshed, then it directly switches to .authorized state. The user code now has access to the token(s).

  • If no good credentials were found and a non-interactive (“silent”) mode was specified, then the client cancels the flow. The user code can treat this state as “not logged in” / “have no access”.

  • If no good credentials were found and an interactive mode was specified, then the client enters .authorizing state and expects the user code to help with authorization UI by presenting a browser (in-app or external).

(See AuthWebViewController in “MMMoth/UI” for a basic implementation.)

The browser should navigate to the endpoint associated with .authorizing state. The user code should be able to intercept all the requests to the associated redirect URL and feed them back to the client via handleAuthorizationRedirect(); it should also report any errors opening the endpoint via handleAuthorizationFailure() and can cancel the flow via cancel().

When the client gets information from authorization server via handleAuthorizationRedirect(), then it either:

  • immediately fails the flow (in case the server returned an explicit error or provided an invalid response);

  • or directly enters .authorized state (in case of an “implicit” flow, that is when responseType does NOT include .code);

  • or begins exchanging the authorization code to token(s) (in case of an “authorization code” flow, that is when responseType does include .code).

A picture for the above:

         ┌─────────────────┐
               idle       
         └─────────────────┘
                  
                  
                                         ┏━━━━━━━━━━━━━━━━━┓
              start()  ─────────────────▶┃   authorized    
                        Have credentials ┗━━━━━━━━━━━━━━━━━┛
                         in the storage
                  
        No good credentials              ┌─────────────────┐
           in the storage   ────────────▶│    cancelled    
                            Silent mode └─────────────────┘
                  
 Interactive mode 
                  
                    The user code opens a browser
             authorizing      and directs it to the
                    specified URL.


 The user code managing the browser calls either of these:

                                         ┌─────────────────┐
                               cancel()─▶│    cancelled    
                                         └─────────────────┘
                                         ┌─────────────────┐
           handleAuthorizationFailure()─▶│     failed      
                                         └─────────────────┘
                                                  
          handleAuthorizationRedirect()           

 Implicit    Auth Code                         
     flow    or Hybrid 
                  flow                    
                       
                 fetchingToken           
                       
                       
                       
              ┏━━━━━━━━━━━━━━━━━┓
          └───▶┃   authorized    
               ┗━━━━━━━━━━━━━━━━━┛

Notes

Initially I wanted to have an OpenID client that would be using an OAuth client under the hood, but this would be more complicated without the OAuth client knowing of OpenID-specific parameters.

  • Initializes without attempting to obtain authorization or check for previously stored credentials.

  • Triggers when state changes.

  • The mode to start the flow in.

    See more
  • A shortcut for start() beginning “authorization code” OpenID flow.

    This flow involves an extra network request but allows to have a refresh token too.

  • A shortcut for start() beginning “implicit” OpenID flow.

    This flow is faster than the “authorization code” flow but retrieves only an ID Token that cannot be refreshed.

  • Called by the user code to start the flow.

    The config is provided here to make it more convenient when it is not available right away, e.g. when it has to be fetched from a backend or an OpenID provider first.

    Depending on the availability of the access/refresh tokens in the store it might skip some states. For example, if an access token is expired but a refresh token is still valid, then it’ll begin with .fetchingToken.

  • Turns the client into .cancelled state unless it is in .authorized already. Safe to call multiple times. Use end() to cancel it even when authorized.

  • Forgets the credentials, if was authorized, cancels the flow otherwise.

  • Called by the user code when the browser got redirected to redirectURL.

  • Called by the user code when the browser cannot open the authorization endpoint.

Refreshing of Tokens

  • Nudges the client to begin refreshing tokens now in case it is waiting between transient token refresh errors. Safe to call regardless of the current state.

    When the client is authorized and the credentials have (almost) expired (case state = .authorized(_, refreshing: true)), then it is going to start refreshing them (if possible), retrying in case of transient errors (e.g. no network). However the timeout between unsuccessful retries is going to grow and it might become relatively large for the user code to simply wait for the client to refresh it. The user code can also have some extra information suggesting that it might make sense to retry earlier. For example, when the user manually triggers to refresh something, then there is a possibility that the connection problems are fixed and the new retry might succeed. This is a call for such a situation.

  • Access and/or ID tokens along with the scopes and response types they were obtained for.

    Equatable for unit tests only, Codable for storage.

    See more
  • Default implementation of MMMothClientNetworking used by MMMothClient.

    Not open for subclassing, but open for composing into your own implementations to simulate network errors or delays in test builds of your app.

    See more
  • Default implementation of MMMothClientTimeSource that can also be used to scale expiration time intervals seen by MMMothClient, something that can be handy for testing.

    See more
  • Optional OpenID-specific parameters for the authorization endpoint. See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest.

    See more
  • What kind of thing we would like to get via a redirect URL when the authorization is successful, i.e. possible values of authorization endpoint’s “response_type” parameter.

    Note that we only support the values listed here as other flows are either not used in a mobile app (“password” or “client_credentials”) or are unknown (extensions).

    See more
  • NSUserDefaults-backed credentials store for MMMothClient.

    See more