Saga Documentation 0.9.674-5

OAuth 2.0 Client Login Flow

Concepts

OAuth 2.0 is the industry-standard protocol for authorization. Saga supports OAuth2.0s provider to login into web application that require Saga user authorization such as the Saga admin. The OAuth settings have to be contained in a global property called oauth_providers. Each provider has to have a unique name and the standard setting as below. Next to the providers, client_urls lists the origins of the apps that may sign users in this way. The flow ends with a redirect to the app, so only an app listed here is ever sent anywhere.

{
  "client_urls": [
    "https://admin.example.com",
    "http://localhost:8080"
  ],
  "provider1": {
    "clientId": "the client id",
    "clientSecret": "the client secret",
    "authorizationUri": "https://authorization url",
    "accessTokenUri": "https://authorization token_uri",
    "issuer": "https://the issuer named in the provider's id tokens",
    "jwksUri": "https://where the provider publishes its signing keys",
    "scopes": [
      "openid",
      "email"
    ]
  },
  "provider2": {
    "clientId": "the client id",
    "clientSecret": "the client secret",
    "authorizationUri": "https://authorization url",
    "accessTokenUri": "https://authorization token_uri",
    "issuer": "https://the issuer named in the provider's id tokens",
    "jwksUri": "https://where the provider publishes its signing keys",
    "scopes": [
      "openid",
      "email"
    ]
  }
}

At the provider level you need to specify the absolute saga authorization redirect URI that is being called by the OAuth service provider once the authorization flow is finished. The path is /users/oauth/:name/add_user, the full URL needs to contain protocol and saga host name, i.e. the host the app reaches the API on.

For Google the values are "issuer": "https://accounts.google.com" and "jwksUri": "https://www.googleapis.com/oauth2/v3/certs", both from its discovery document. The openid scope is what makes the provider return an id token at all, email what puts the address into it.

Who signed in is read from the id token, once its signature checks out against the keys at jwksUri and it was issued by issuer for this clientId. The user is the one linked to the provider's account (its subject, kept in oauth_subjects on the user). The first time there is no link yet, the user with the email the provider reports is taken and linked, but only when the provider marks the email as verified.

The flow in three steps: the app asks GET /users/oauth/:name where to send the browser, the provider sends the browser back to the API, and the API sends it on to the app's client_url with a one-time oauth_code in the query. The app exchanges that code for the tokens with POST /users/oauth/exchange. No token ever travels in a URL, the code is good for one exchange and one minute.

HTTP API

It requires the anonymous role to contain {path: "/users/oauth", action: "*", allow: true} for the oauth flow.

GET /users/oauth/

Returns an array providing the names of the OAuth providers set in the global property oauth_providers.

Example:

[
  "provider1",
  "provider2"
]

GET /users/oauth/:name

Start the OAuth flow. Returns the URL of the provider to send the browser to. Requires the following query parameter:

  • client_url the absolute URL of the page in the app the browser comes back to once the flow is finished, a hash route included, like https://admin.example.com/#/oauth. Its origin has to be listed in client_urls of the oauth_providers global, otherwise the request is refused with a 400.

The API keeps a random state for ten minutes and refuses a callback that does not carry one it handed out.

GET /users/oauth/:name/add_user

This is the OAuth 2.0 redirect URL that is called by the OAuth provider, a Saga app never has to use it. It exchanges the provider's code, looks up the verified Saga user with the email the provider reports, and sends the browser on to the client_url given in the GET /users/oauth/:name call, with a query appended:

  • oauth_code on success, a one-time code good for one minute, i.e. https://admin.example.com/#/oauth?oauth_code=3f2c…
  • oauth_error otherwise, the message, i.e. …#/oauth?oauth_error=User+with+email+foo%40bar.com+not+found

POST /users/oauth/exchange

Turns the oauth_code into the session. The code is taken away with the first exchange, a second one is a 404.

{
  "code": "3f2c…"
}

Answers with the same payload as POST /users/login:

{
  "_id": "user_01KRMF315PZM052ZH48P215BND",
  "name": "name",
  "email": "foo@bar.com",
  "accessToken": "eyJ562eXAiOiJKV1QJhbGciOiJIUzI1NiJ9…",
  "refreshToken": "eyJ0eXAiOiweweJKV1QiLCJhbGciOiJIUzI1NiJ9…",
  "roles": ["650db9ae2c30ffe34537d3cf"]
}