User Tools

Site Tools


en:outskirts:mages-peaks:sso

This is an old revision of the document!


The Laelith Authentication ("SSO")

Introduction

Users authentication in the Laelith ecosystem is based on the OAuth 2 and OpenID Connect industry standards, with the addition of support for the PKCE standard for improved security.

This is well suited to the context of distributed services that we have in Laelith.

OpenID Connect

The OAuth 2 and OpenID Connect (aka “OIDC”) standards can be intimidating at first, because they cover many use cases, each case being covered by a co-called “flow”. In the context described here, we will simply limit ourselves to a web app (like a Single Page App aka “SPA”) that wants to identify the user and get an Access Token to talk to the Laelith API.

To do this, the OIDC “Authorization Flow” is used. This flow is implemented in two calls to the Laelith Identity Server (aka “ID Server” at https://id.laelith.com).

  • The first call allows the web app to receive a temporary Authorization Code by sending the user to the Identity Server and getting him back through a Redirect.
  • Using this Authorization Code, the web app can then place a second call, which will return an Access Token (to talk to the Laelith API) and an ID Token (that describes the user’s details).

The Access Token and the ID Token follow the JWT open standard.

Some UX Considerations

Before getting started, there is a question you need to ask yourself: “Is there any interest for the user to use my app anonymously?”

You will find that the answer is generally: “no”.

The Laelith architecture is made of many independent apps that rely on the Laelith Identity Server to authenticate the user. So the recommended strategy is when your app loads a page, it should check if is has valid Tokens. If it is not the case, initiate right away an Open ID Connect flow to connect the user.

Getting Started Easily

To make your developer life easier, we provide a Javascript library that hides all the complexity and does all the heavy-lifting for you. In just a few lines of Javascript, you will be up and running in a secure manner. This is described in the next section.

The “Under the Hood” sections later down provide more details about the OIDC end-points, and will be useful only if you need to talk directly to the API end-points.

Front-end Javascript Laelith-Auth Library

This library implements the complete Laelith Authorization Flow and manages the token acquisition. It can be used in any Laelith Front-end application without having to redevelop the Authorization features.

The laelithauth library can be used directly using the “compiled” javascript version, laelithauth.js, made available by the Laelith Map application. (Note that it is planned to move this library to the Identity Server in some time).

Using the “compiled” laelithauth.js library

Insertion in index.html

The laelithauth.js library should be included in the <head> section of your index.html, and provided with configuration information.

The typical setup is:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>Laelith Map</title>
  <script src="/js/config.js"></script>
  <script src="https://map.laelith.com/build/laelithauth.js" 
    id="laelithauth" clientid="map.laelith.com" autostart="true"></script>

The Auth library will trigger page reloads as needed, in the process of authenticating and authorizing the user, so it is recommended to include it at the top of the page to avoid waiting for other libs to needlessly load.

Parameterizing the library

The parameters are:

  • autostart: (default false) immédiately performs Authorization upon load.
  • clientid: (required) the ClientID of your front-end application, that must be registered in the Laelith ID server.
  • redirect: (default: currentUrl without fragment, querystring, trailing slash) the redirectURL to use for receiving the AuthorizationCode (see OpenID connect Authorization flow), that must also be registered in the Laelith ID server.
  • idserver: (default https://id.laelith.com) the URL of the ID server.
  • debug: (default false) will print detailed messages in the console.

In the direct use of the laelithauth.js lib case, they may be passed as attributes to an HTML element whose id=laelithauth, for example:

<script src="https://map.laelith.com/build/laelithauth.js" 
  id="laelithauth"
  autostart="true"
  identity="https://id.laelith.com"
  clientid="map.laelith.com"
  redirect="http://map.laelith.com"></script>

These values are superseded by any values set in a global gConfigobject with the following properties:

var gConfig = {
    identity: "https://id.laelith.com", /* Identity Server */
    clientid: "map.laelith.com",        /* OIDC client_id */
    redirect: "https://map.laelith.com",/* optional */
    autostart: true,                    /* default false */
    debug: false,                       /* default false */

It is for example possible to set the “default” (prod) parameters in the index.html, and override them in your dev environment using your private config.js.

Using this way of specifying parameters, it is recommended to set autostart to true, as it allows to start the Authorization process immediately when the page loads.

The parameters can also be set directly from Javascript code. In this case, it is recommended to set autostart to false, to prevent operations startup before the values are set.

Use for this purpose the exported global object: laelithauth.authManager that exposes:

  setIdentity: (identity: string) => void;
  getIdentity: () => string;
  setClientId: (clientid: string) => void;
  getClientId: () => string;
  setRedirectUrl: (redirect: string) => void;
  getRedirectUrl: () => string;
  run: () => void;

As an example:

laelithauth.authManager.setClientId("map.laelith.com");
laelithauth.authManager.run();      
   // Executes the Auth process - may reload the page

The redirectUrl param is optional - if absent, it will be inferred from the document URL, by taking the origin/pathname parts of the URL (e.g. http://localhost:8090 in my VSCode dev environment).

To help debugging auth issues, the parameters used in the authorization process are logged in the Javascript console. You may need to check a “persist logs” option to be able to keep it across redirects.

Additional messages are printed out with the debug=true option.

The Laelith Auth API

The Laelith Auth library provides an API to allow the front-end application to:

  • Determine the Authorization status of the user
  • Obtain the Bearer Token required for accessing back-end servers (API server…)
  • Obtain User information (userid, username, avatar, connected Player Character
  • Obtain User Authorizations (groups – and later, if we confirm that, specific authorization grants, such as "can modify Lot-257", "can access Guild-22" ).

It can be used from any javascript-based application.

The global variable referencing the Auth Manager is:

laelithauth.authManager

It exposes the AuthManager interface described below (see formal Typescript definition in laelithauth.d.ts)

/**
 * This is the interface exposed to the Library users, by the laelithauth.authManager object.
 */
export interface AuthManager {
    /** @returns true if the Auth Process is complete and was successful. */
    isLogged: () => boolean;
    /** @returns the raw Access Token for use in an Authorization: Bearer header when accessing protected resources. */
    getRawAccessToken: () => string | null;
    /** @returns the User Auths info if the Auth Process is complete and was successful. */
    getUserInfo: () => IdToken | null;
    /**
     * The registered callback will be invoked when the Auth Process is complete.
     * It is also invoked if the Auth Process is already complete.
     */
    onAuthorized: (callback: (id: IdToken) => void) => void;
    /** Forces a logout of the User. Will trigger a redirect to the ID server login page. */
    logout: () => void;
    /**
     * Triggers a refresh of the Access Token, using the Refresh Token
     * (if the Auth Process is complete and successful,
     * and if the ID server provided a Refresh Token).
     * <p/>
     * Will invoke the onAuthorized callback again (The IdToken may have changed).
     */
    refresh: () => void;
}
 
/** This is the interface exposed for JS-configuration. */
export interface AuthConfigAccess {
    /** Identity Server to use (default https://id.laelith.com) */
    setIdentity: (identity: string) => void;
    getIdentity: () => string;
    /** client_id that should match the redirect_url on the selected identity server. */
    setClientId: (clientid: string) => void;
    getClientId: () => string;
    setRedirectUrl: (redirect: string) => void;
    getRedirectUrl: () => string;
    /** start the Auth process.  */
    run: () => void;
}
 
export interface AccessToken {
    iss: string;    // Issuer (the ID server)
    aud: string;
    sub: number;    // Subject
    exp: number;    // Expiration time (in Secs since 1/1/70)
    groups: string[];
}
 
export interface PlayerCharacter {
    id: number;
    username: string;
    avatar: string;
}
 
export interface IdToken {
    iss: string;    // Issuer (the ID server)
    aud: string;
    sub: number;    // Subject (User ID)
    exp: number;    // Expiration time (in Secs since 1/1/70)
    userid: number;
    username: string;
    avatar: string;
    character: PlayerCharacter;
    groups: string[];
}
 
/** Provide Global access to the AuthManager. */
export declare const authManager: AuthManager & AuthConfigAccess;

Under the Hood: the Identity Server End-points

The Authorization Server (in OIDC lingo) is the Laelith Identity Server (https://id.laelith.com). It provides a small OIDC api that is described below. The Identity Server uses its own session management (a basic PHP session) to perform the user’s authentication. It also provides a number of services to the user, such as managing his Profile.

Some recommendations before you get started

Don't try to chew too much right away! At first, start by providing only the compulsory parameters. Don't bother about the nonce, state or code_verifier parameters. Add these only once the complete flow works, to secure your work.

Another thing: to avoid being forced to test in a production environment, you can use a special test url as a redirect uri. Say your app redirect uri is https://myapp.laelith.com, the Identity Server will consider that the http://myapp.test.laelith.com is valid too. So use this redirect uri during your development and edit your /etc/host file to define this domain and direct it to your local machine, for example like this:

::1     localhost myapp.test.laelith.com

On top of this, you can use a custom port. Often, local development makes you run a local server under a special port. It's OK to use this port in your redirect uri as long as it uses a myapp.test.laelith.com domain: http://myapp.test.laelith.com:1234 will be considered as valid and will be used in redirections.

the Authorization End-point

The web app starts by asking for an Authorization Code. The redirect that happens ensures that the code is sent to the right place.

GET or POST /api/realms/laelith/authorize

(the POST can be done as application/x-www-form-urlencoded or as application/json)
(if you do a GET, don't forget to url-encode each parameter)

  • response_type=code ← required
  • scope=openid ← the scope must include openid
  • client_id=map.laelith.com ← must be one of the allowed client ids
  • redirect_uri=https://map.laelith.com ← must correspond exactly to one of the client urls known to the Identity Server. If passed in a GET query, don't forget to url-encode it!
  • state=YourStateString ← an optional string that allows the client to store some state to mitigate CSRF or XSRF attacks
  • nonce=YourNonceString ← an optional random string that will be used to increase the entropy of the JWT tokens and mitigate replay attacks. 32 random A-Za-z0-9 chars are good.
  • code_challenge=AChallenge ← implementation of the PKCE protocol. This is the base64(sha256(code_verifier)) with code_verifier = a random string of at least 43 characters in A-Za-z0-9. Keep code_verifier in a safe place on the client side, you will need it when asking for the Access Token (see below). Alternatively, you can provide a code_verifier parameter instead with the code_verifier value in plain text, but this is less secure against a MITM attack.
  • returnto=/go/here ← an optional string that will be returned as is to the client. Useful for the client to remember where to go next. If passed in a GET query, don't forget to url-encode it!
  • anchor=blabla ← same idea as returnto, but for the anchor part

The end-point will return:

  • response_type=code
  • code=theAuthorizationCode ← a unique random string
  • the state, nonce, returnto and anchor values if they were provided in the query

Example:

GET https://id.laelith.com/api/realms/laelith/authorize?
  response_type=code
  &scope=openid
  &client_id=map.laelith.com
  &redirect_uri=https%3A%2F%2Fmap.laelith.com
  &state=State42
  &code_challenge=OGIzNWIyZmE0Mjk3ZDU1N2Y0ODZhZjRhNDUyNWQyYzM3ODA3NmU3ZDMxMjkyMDJiNzJjMjFhMWRjZmNjMzY5YQ==
  &nonce=zWl8Ud6HzV59OXA7PQ5L7SptHRj7usf3
  &returnto=%2F
  &anchor=256

HTTP/1.1 302 Found
Location: https://map.laelith.com?
    response_type=code
    &code=rlu8Vq2K66S2T4LGKChLbDHridDUkEUI
    &state=State42
    &nonce=zWl8Ud6HzV59OXA7PQ5L7SptHRj7usf3
    &returnto=%2F
    &anchor=256

Access Token End-point

Once the Authorization Code has been issued, it can be exchanged for an Access Token and an ID Token. This consumes the Authorization Code.

POST /api/realms/laelith/token

(the POST can be done as application/x-www-form-urlencoded or as application/json)

  • grant_type=authorization_code ← required
  • code=rlu8Vq2K66S2T4LGKChLbDHridDUkEUI ← the code that was returned by the Authorization end-point
  • client_id=map.laelith.com ← must be client id that was used to generate the code
  • redirect_uri=https://map.laelith.com ← even if it won’t be used here, OpenID Connect requires it
  • state=YourStateString ← see explanation in previous end-point
  • nonce=YourNonceString ← see explanation in previous end-point
  • code_verifier=CodeVerifier ← the code_verifier string that you used to compute the code_challenge parameter for the previous end-point
  • returnto=/go/here ← see explanation in previous end-point
  • anchor=blabla ← see explanation in previous end-point

The end-point will return:

  • token_type=Bearer
  • access_token=eyJ0eX…< ← Access Token, see below
  • id_token=iOiJS…< ← ID Token, see below
  • expires_in=86400 ← TTL in seconds
  • refresh_token=vyWBd… ← Refresh Token, to store in a safe place
  • the state, returnto and anchor values if they were provided in the query

The Access Token

The Access Token is a JWT token that can be base64-decoded (https://jwt.io is convenient for introspection and debugging). Payload:

{
  "iss": "id.laelith.com",      ← issuer
  "aud": "api.laelith.com",     ← target audience
  "sub": 1,                     ← user ID
  "groups": [                   ← Role groups of the user
    "admin",
    "user",
    "dev",
    "cartographer"
  ],
  "exp": 1656971633,                ← expiration date
  "iat": 1656885233,                ← creation date
  "nonce": "zWl8Ud6HzV…PHRj7usf3"   ← the nonce you provided
}

The RS256 signature must be verified by the client using the public key that can be retrieved from a specific end-point described later in this document.

The ID Token

The ID Token is a JWT token that provides details about the user. It can also be used as a stateless session. Payload:

{
  "iss": "id.laelith.com",          ← issuer
  "aud": "laelith.com",             ← target audience (all Laelith)
  "sub": 1,                         ← user ID
  "exp": 1656971633,                ← expiration date
  "iat": 1656885233,                ← creation date
  "nonce": "zWl8Ud6HzV…PHRj7usf3",  ← the nonce you provided
  "username": "YannZeRookie",       ← Account username
  "groups": [                       ← Role groups of the user
    "admin",
    "user",
    "dev",
    "cartographer"
  ],
  "avatar": "https://img….me.jpg",  ← avatar image
  "character": {                    ← active character (if any)
    "id": 2,                        ← character ID
    "username": "Kahny",            ← character name
    "avatar": "https://…/kahny.jpg" ← character image
  }
}

Refresh Token End-point

It works pretty much like the Access Token call (it’s the same end-point):

POST /api/realms/laelith/token
  • grant_type=refresh_token ← required
  • refresh_token=vyWBdshYgwcG…iYu7yRdKU58m ← the Refresh Token that was returned with the Access Token
  • client_id=map.laelith.com ← must be client id that was used to generate the code
  • scope=openid ← OIDC requires this, don’t ask me why
  • state=YourStateString ← see explanation in previous end-point
  • nonce=YourNonceString ← see explanation in previous end-point
  • returnto=/go/here ← see explanation in previous end-point
  • anchor=blabla ← see explanation in previous end-point

The result is the same as with the Access Token. Note that the Refresh Token will be consumed, and that a new one will be issued.

Note: currently, the lifetime of a Refresh Token is 7 times the one of an Access Token: 7 days vs 1 day.

Public Key End-point

GET /api/realms/laelith/pub

Return the public key in text/plain format so that the tokens can be validated (which is recommended).

You want to make a copy of this public key and store it in your application, for example hard-coded in a configuration file. This will avoid making a round-trip to the server every time you want to validate a JWT token, and it will prevent man-in-the-middle attacks.This will also allow you to use a different set of keys for your own development environment.

This end-point is merely here for your convenience, so that you can grab the public key easily.

—–BEGIN PUBLIC KEY—– MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApULANXv8R2sh9a/eF8GBjDVtEkXB1q4TYIGtF+fP98YR59PljxPfBjMz/tZp2pu65VMWWE5kZqWo/E5COfRF



U/6tqIA6u9MfYnSFRjHq+Lx9O4o38NCYB6dTYef9+fb9PEhKEJjMsipWW9ychMlfXZO4zsO2QQ9AxBoGwSrlC/IC/tM4wgvDchLKxLivKVfxPe5tL7yZbkY07haq+c1k fMxNv1G8Eh2ckLfMoTMW0vMeVhkZGiuVZ1U5JwwBVDd8jGm+9XOTUtjidRoCbvYMY35k7MYvXWQY6IS/sDGnQtsCAwEAAQ==
—–END PUBLIC KEY—–

Important Advises for Server-side Apps

If your app runs server-side (e.g. a PHP, Python, RoR, Phoenix, etc. app) and that you decide to use the two end-points rather than using the little Javascript library, you need to understand some key concepts.

The Authorization End-point must be called by the Web browser. In other words, it's a client-to-server GET call, not a server-to-server one. This is required for several reasons. The main one is that the OIDC Authorization Flow relies on a redirect scheme to identify your app, so this redirection must be run by the Web browser. The other reason is that the user may already be identified with the Identity Server in the Web browser. The corresponding session cookie is only known from the Web browser, that's why it is the one who should make that call.

PHP example (in a PSR-7 context, in a local development config where the PHP app runs on port 8080):

$client_id = 'myapp.laelith.com';
$redirect_uri = urlencode('http://myapp.test.laelith.com:8080');
$response->withHeader('Location', "https://id.laelith.com/api/realms/laelith/authorize?response_type=code&scope=openid&client_id=$client_id&redirect_uri=$redirect_uri")->withStatus(302);

If everything goes well, the Web browser will return to your app with an url that will look like:

http://myapp.test.laelith.com:8080?response_type=code&code=9VyGHvt7EqDvDKUoEjk6BBk3ih3hjCs3

The Access Token End-point must be called by your server-side code

Congratulations, you did the hardest part: get an Authorization code! Now your app should place a server-to-server POST call to the Access Token End-point with that code.

use GuzzleHttp\Client;
$client_id = 'myapp.laelith.com';
$redirect_uri = urlencode('http://myapp.test.laelith.com:8080');
$params = $request->getQueryParams();
if (!empty(params['code'])) {
    $client = new Client();
    $res = $client->post('https://id.laelith.com/api/realms/laelith/token', [
        GuzzleHttp\RequestOptions::JSON => [
            'grant_type' => 'authorization_code',
            'code' => $params['code'],
            'client_id' => $client_id,
            'redirect_uri' => $redirect_uri,
        ] 
    ]);
    // (here you're supposed to test the error codes)
    // Get the data:
    $data = json_decode(res->getBody(), true);
    $access_token_jwt = $data['access_token'];
    $refresh_token = $data['refresh_token'];
    $id_token_jwt = data['id_token'];
}

Note: in PHP, the firebase/php-jwt package is great to decode the Access Token and the ID Token.

Once you collected the tokens, place them in a safe place server-side and tie them to a session

You need to place the tokens in a safe place on the server side (for example, in a database). You also need to verify their expiration and use the Refresh Token (if it is still valid) to get new tokens if they expired.

In a server-side app, you generally tie the client-side context to the server side by using some session mecanism.

For example in PHP, a simple solution could be to store the (decoded) tokens in session itself:

$_SESSION['tokens'] = [
    'access_token' => $access_token,
    'refresh_token' => $refresh_token,
    'id_token' => $id_token,
];

You are done! Redirect to the main (or the current) page !

Even if it is ephemeral, you don't want to keep the Authorization code in the browser url, so redirect to your site page. Since your session was set, you will get all the Tokens right away and know who the user is.

en/outskirts/mages-peaks/sso.1701642746.txt.gz · Last modified: 2024/10/28 08:00 (external edit)