en:outskirts:mages-peaks:sso
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| en:outskirts:mages-peaks:sso [2023/11/12 21:30] – removed - external edit (Unknown date) 127.0.0.1 | en:outskirts:mages-peaks:sso [2024/10/28 08:00] (current) – external edit 127.0.0.1 | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | ===== The Laelith Authentication (" | ||
| + | |||
| + | ===== Introduction ==== | ||
| + | |||
| + | Users authentication in the Laelith ecosystem is based on the [[https:// | ||
| + | |||
| + | 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:// | ||
| + | |||
| + | * 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 [[https:// | ||
| + | |||
| + | ===== 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: " | ||
| + | |||
| + | 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 " | ||
| + | |||
| + | ===== 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, '' | ||
| + | |||
| + | ==== Using the “compiled” laelithauth.js library ==== | ||
| + | |||
| + | === Insertion in index.html === | ||
| + | |||
| + | The '' | ||
| + | |||
| + | **The typical setup is**: | ||
| + | |||
| + | <code html5> | ||
| + | < | ||
| + | <html lang=" | ||
| + | <script src="/ | ||
| + | <script src=" | ||
| + | id=" | ||
| + | </ | ||
| + | |||
| + | 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: | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | In the direct use of the '' | ||
| + | |||
| + | <code html5> | ||
| + | <script src=" | ||
| + | id=" | ||
| + | autostart=" | ||
| + | identity=" | ||
| + | clientid=" | ||
| + | redirect=" | ||
| + | </ | ||
| + | |||
| + | These values are superseded by any values set in a global '' | ||
| + | |||
| + | <code javascript> | ||
| + | var gConfig = { | ||
| + | identity: " | ||
| + | clientid: " | ||
| + | redirect: " | ||
| + | autostart: true, /* default false */ | ||
| + | debug: 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 '' | ||
| + | |||
| + | The parameters can also be set directly from Javascript code. In this case, it is recommended to set '' | ||
| + | |||
| + | Use for this purpose the exported global object: '' | ||
| + | |||
| + | <code javascript> | ||
| + | setIdentity: | ||
| + | getIdentity: | ||
| + | setClientId: | ||
| + | getClientId: | ||
| + | setRedirectUrl: | ||
| + | getRedirectUrl: | ||
| + | run: () => void; | ||
| + | </ | ||
| + | |||
| + | As an example: | ||
| + | |||
| + | <code javascript> | ||
| + | laelithauth.authManager.setClientId(" | ||
| + | laelithauth.authManager.run(); | ||
| + | // Executes the Auth process - may reload the page | ||
| + | </ | ||
| + | |||
| + | The '' | ||
| + | |||
| + | 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 '' | ||
| + | |||
| + | |||
| + | ==== 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 '' | ||
| + | |||
| + | It can be used from any javascript-based application. | ||
| + | |||
| + | The global variable referencing the Auth Manager is: | ||
| + | |||
| + | '' | ||
| + | |||
| + | It exposes the '' | ||
| + | |||
| + | <code javascript> | ||
| + | /** | ||
| + | * 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: | ||
| + | getRawAccessToken: | ||
| + | /** @returns the User Auths info if the Auth Process is complete and was successful. */ | ||
| + | getUserInfo: | ||
| + | /** | ||
| + | * The registered callback will be invoked when the Auth Process is complete. | ||
| + | * It is also invoked if the Auth Process is already complete. | ||
| + | */ | ||
| + | onAuthorized: | ||
| + | /** 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:// | ||
| + | setIdentity: | ||
| + | getIdentity: | ||
| + | /** client_id that should match the redirect_url on the selected identity server. */ | ||
| + | setClientId: | ||
| + | getClientId: | ||
| + | setRedirectUrl: | ||
| + | getRedirectUrl: | ||
| + | /** start the Auth process. | ||
| + | run: () => void; | ||
| + | } | ||
| + | |||
| + | export interface AccessToken { | ||
| + | iss: string; | ||
| + | aud: string; | ||
| + | sub: number; | ||
| + | exp: number; | ||
| + | groups: string[]; | ||
| + | } | ||
| + | |||
| + | export interface PlayerCharacter { | ||
| + | id: number; | ||
| + | username: string; | ||
| + | avatar: string; | ||
| + | } | ||
| + | |||
| + | export interface IdToken { | ||
| + | iss: string; | ||
| + | aud: string; | ||
| + | sub: number; | ||
| + | exp: number; | ||
| + | userid: number; | ||
| + | username: string; | ||
| + | avatar: string; | ||
| + | character: PlayerCharacter; | ||
| + | groups: string[]; | ||
| + | } | ||
| + | |||
| + | /** Provide Global access to the AuthManager. */ | ||
| + | export declare const authManager: | ||
| + | |||
| + | </ | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ===== Under the Hood: the Identity Server End-points ===== | ||
| + | |||
| + | The Authorization Server (in OIDC lingo) is the Laelith Identity Server (https:// | ||
| + | |||
| + | ==== 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 '' | ||
| + | |||
| + | Another thing: to avoid being forced to test in a production environment, | ||
| + | |||
| + | < | ||
| + | ::1 | ||
| + | </ | ||
| + | |||
| + | 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:// | ||
| + | ===== 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 / | ||
| + | </ | ||
| + | |||
| + | (the POST can be done as '' | ||
| + | (if you do a GET, don't forget to url-encode each parameter) | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | The end-point will return: | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * the '' | ||
| + | ==== Example: ==== | ||
| + | |||
| + | < | ||
| + | GET https:// | ||
| + | response_type=code | ||
| + | & | ||
| + | & | ||
| + | & | ||
| + | & | ||
| + | & | ||
| + | & | ||
| + | & | ||
| + | & | ||
| + | |||
| + | HTTP/1.1 302 Found | ||
| + | Location: https:// | ||
| + | response_type=code | ||
| + | & | ||
| + | & | ||
| + | & | ||
| + | & | ||
| + | & | ||
| + | </ | ||
| + | |||
| + | ===== 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 / | ||
| + | </ | ||
| + | |||
| + | (the POST can be done as '' | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | The end-point will return: | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * the '' | ||
| + | |||
| + | ==== The Access Token ==== | ||
| + | |||
| + | The Access Token is a JWT token that can be base64-decoded (https:// | ||
| + | |||
| + | <code javascript> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | 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: | ||
| + | |||
| + | <code javascript> | ||
| + | { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | ], | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ===== Refresh Token End-point ===== | ||
| + | |||
| + | It works pretty much like the Access Token call (it’s the same end-point): | ||
| + | |||
| + | < | ||
| + | POST / | ||
| + | </ | ||
| + | |||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | 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 / | ||
| + | </ | ||
| + | |||
| + | Return the public key in '' | ||
| + | |||
| + | You want to make a copy of this public key and store it in your application, | ||
| + | |||
| + | This end-point is merely here for your convenience, | ||
| + | |||
| + | > | ||
| + | > | ||
| + | > … | ||
| + | > | ||
| + | > | ||
| + | > | ||
| + | |||
| + | ===== 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 [[https:// | ||
| + | |||
| + | <code php> | ||
| + | $client_id = ' | ||
| + | $redirect_uri = urlencode(' | ||
| + | $response-> | ||
| + | </ | ||
| + | |||
| + | If everything goes well, the Web browser will return to your app with an url that will look like: | ||
| + | |||
| + | http:// | ||
| + | |||
| + | **The Access Token End-point must be called by your server-side code** | ||
| + | |||
| + | Congratulations, | ||
| + | |||
| + | <code php> | ||
| + | use GuzzleHttp\Client; | ||
| + | $client_id = ' | ||
| + | $redirect_uri = urlencode(' | ||
| + | $params = $request-> | ||
| + | if (!empty(params[' | ||
| + | $client = new Client(); | ||
| + | $res = $client-> | ||
| + | GuzzleHttp\RequestOptions:: | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ] | ||
| + | ]); | ||
| + | // (here you're supposed to test the error codes) | ||
| + | // Get the data: | ||
| + | $data = json_decode(res-> | ||
| + | $access_token_jwt = $data[' | ||
| + | $refresh_token = $data[' | ||
| + | $id_token_jwt = data[' | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Note: in PHP, the firebase/ | ||
| + | |||
| + | **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: | ||
| + | |||
| + | <code php> | ||
| + | $_SESSION[' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ]; | ||
| + | </ | ||
| + | |||
| + | **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. | ||
