(auth-details)= # Authentication Details See {doc}`../explanations/security` for an overview. This page addresses technical details relevant to: - Client authentication flow * Customizing the various lifetimes (timeouts) in the system * Horizontally scaling deployments ## Client authentication flow In this section we'll use the command-line HTTP client [httpie](https://httie.io/) and [jq](https://stedolan.github.io/jq/) to parse the JSON responses. Some Tiled servers are configured handle credentials directly. Others are configured to refer the user to a web browser to authenticate with a third party (e.g. ORCID, Google) and return to Tiled with a token. We will demonstrate each in turn. To test the first kind, we'll start a tiled server with a demo "toy" authentication system to work against. Use this example configuration that is included with the tiled source code, and start a server like so. ```{eval-rst} .. literalinclude:: ../../../example_configs/toy_authentication.yml :caption: example_configs/toy_authentication.py ``` ``` ALICE_PASSWORD=secret1 BOB_PASSWORD=secret2 CARA_PASSWORD=secret3 tiled serve config example_configs/toy_authentication.yml ``` To test the second kind, we'll use `https://tiled-demo.blueskyproject.io`, which is configured to use [ORCID](https://orcid.org/) for authentication. ### Scenario 1: Authenticator Directly Handles Credentials An initial handshake with the `/` route tells us that authentication is required on this server. This is one authentication provider, and it expects (HTTP basic) password authentication. The `auth_endpoint` tells us where to POST our credentials. ``` $ http :8000/api/v1/ | jq .authentication { "required": true, "providers": [ { "provider": "toy", "mode": "password", "links": { "auth_endpoint": "http://localhost:8000/api/v1/auth/provider/toy/token" }, "confirmation_message": null } ], "links": { "whoami": "http://localhost:8000/api/v1/auth/whoami", "apikey": "http://localhost:8000/api/v1/auth/apikey", "refresh_session": "http://localhost:8000/api/v1/auth/session/refresh", "revoke_session": "http://localhost:8000/api/v1/auth/session/revoke/{session_id}", "logout": "http://localhost:8000/api/v1/auth/logout" } } ``` Exchange username/password credentials for "access" and "refresh" tokens. ``` $ http --form POST :8000/api/v1/auth/provider/toy/token username=alice password=secret1 > tokens.json ``` The content of `tokens.json` looks like ```json {"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsImV4cCI6MTYyODEwNTQyMiwidHlwZSI6ImFjY2VzcyJ9.bd8T3yYo9LDxBaCB3luSbSBh4dcVJDfXTFtW9s6aa3Q", "expires_in":900, "refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsInR5cGUiOiJyZWZyZXNoIiwiaWF0IjoxNjI4MTE4OTIyLjU3NTM4NSwic2lkIjoxMzgwNjIwMjE2MTg3ODQyMTM2NzgwNzQ2NjEwNzE3NTAxMzEyNTMsInNjdCI6MTYyODExODkyMi41NzUzODV9.ms0y8x4csMVvyDozCCa2RE48nRDEd16RFK9RbrsBS5E", "refresh_token_expires_in":604800, "token_type":"bearer" } ``` Make an authenticated request using that access token. ``` $ http GET :8000/api/v1/metadata/ "Authorization:Bearer `jq -r .access_token tokens.json`" HTTP/1.1 200 OK content-length: 239 content-type: application/json date: Wed, 04 Aug 2021 19:17:56 GMT etag: c1f7b0169f6baabad75f80a0bf6a2656 server: uvicorn server-timing: app;dur=3.9 set-cookie: tiled_csrf=1-Cpa1WcwggakZ91FtNsscjM8VO1N1znmuILlL5hGY8; HttpOnly; Path=/; SameSite=lax { "data": { "attributes": { "count": 2, "metadata": {}, "sorting": null, "specs": null }, "id": "", "links": { "search": "http://localhost:8000/api/v1/search/", "self": "http://localhost:8000/api/v1/metadata/" }, "meta": null, "type": "tree" }, "error": null, "links": null, "meta": {} } ``` When the access token expires (after 15 minutes, by default) requests will be rejected like this. ``` $ http GET :8000/api/v1/metadata/ "Authorization:Bearer `jq -r .access_token tokens.json`" HTTP/1.1 401 Unauthorized content-length: 53 content-type: application/json date: Wed, 04 Aug 2021 19:22:07 GMT server: uvicorn server-timing: app;dur=2.7 set-cookie: tiled_csrf=6sPHOrjBRzZOiSuXOXNtaDNyNNeqQj86nPIXf7X3C1M; HttpOnly; Path=/; SameSite=lax { "detail": "Access token has expired. Refresh token." } ``` Exchange the refresh token for a fresh pair of access and refresh tokens. ``` $ http POST :8000/api/v1/auth/session/refresh refresh_token=`jq -r .refresh_token tokens.json` > tokens.json ``` And resume making requests with the new access token. To experiment with token expiry and renewal, it can be useful to tune the various "max age" parameters very low---10 seconds or so. The next section describes how to configure these parameters. ### Scenario 2: Authenticator Refers to a Third-Party Identity Provider An initial handshake with the `/` route tells us that this server uses `"external"` authentication. ``` $ http https://tiled-demo.blueskyproject.io/api/v1/ | jq .authentication.type "external" ``` Elsewhere in this same response, we can find the authentication endpoint for this external identity provider. ``` $ http https://tiled-demo.blueskyproject.io/api/v1/ | jq .authentication.endpoint "https://orcid.org/oauth/authorize?client_id=APP-0ROS9DU5F717F7XN&response_type=code&scope=openid&redirect_uri=https://tiled-demo.blueskyproject.io/auth/code", ``` Navigate to this address in a web browser, log in when prompted, and authorize Tiled when prompted. You will be redirected to a page at `https://tiled-demo.blueskyproject.io/auth/code?code=[redacted]` and shown a valid refresh token from Tiled that encodes your ORCID username. Exchange the refresh token for an access token and a fresh refresh token like so. ``` $ http POST https://tiled-demo.blueskyproject.io/api/v1/auth/session/refresh refresh_token="TOKEN PASTED FROM WEB BROWSER" > tokens.json ``` From here, everything follows the same as in Scenario 1, above. ## Configure session lifetime parameters The server implements "sliding sessions". The following are tunable: * Maximum inactive session age --- Time after which inactive sessions (sessions that have not refreshed tokens) will time out. * Maximum session age --- Even *active* sessions are timed out after this limit, and the user is required to resubmit credentials. * Access token max age --- This controls how often fresh access token have to be re-issued. The process is transparent to the user and just affects performance. An *access* token cannot be revoked, so its lifetime should be short. The default is 15 minutes. These are tuned, respectively, by the following configuration parameters, given in units of seconds. The default values are shown. ```yaml authentication: refresh_token_max_age: 604800 # one week session_max_age: 31536000 # 365 days access_token_max_age: 900 # 15 minutes ``` and may also be set via the environment: ``` TILED_REFRESH_TOKEN_MAX_AGE TILED_SESSION_MAX_AGE TILED_ACCESS_TOKEN_MAX_AGE ``` See also {doc}`service-configuration`. ## Set and Rotate the Signing Key The access tokens are signed using a secret key that, by default, is generated automatically at server startup. **Set the secret manually to ensure that existing tokens remain valid after a server restart or across horizontally-scaled deployments of multiple servers.** ```{note} When generating a secret, is important to produce a difficult-to-guess random number, and make it different each time you start up a server. Two equally good ways to generate a secure secret... With ``openssl``: openssl rand -hex 32 With ``python``: python -c "import secrets; print(secrets.token_hex(32))" ``` Apply it by including the configuration ```yaml authentication: secret_keys: - "SECRET" ``` or by setting the ``TILED_SERVER_SECRET_KEYS``. If you prefer, you can extract the keys from the environment like: ```yaml authentication: secret_keys: - "${SECRET}" # will be replaced by the environment variable ``` To rotate keys with a smooth transition, provide multiple keys ```yaml authentication: secret_keys: - "NEW_SECRET" - "OLD_SECRET" ``` or set ``TILED_SERVER_SECRET_KEYS`` to ``;``-separated values, as in ``` TILED_SERVER_SECRET_KEYS=NEW_SECRET;OLD_SECRET ``` The first secret value is always used to *encode* new tokens, but all values are tried to *decode* existing tokens until one works or all fail.