Authentication Details
See 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 and 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.
authentication:
providers:
- provider: toy
authenticator: tiled.authenticators:DictionaryAuthenticator
args:
users_to_passwords:
alice: ${ALICE_PASSWORD}
bob: ${BOB_PASSWORD}
cara: ${CARA_PASSWORD}
confirmation_message: "You have logged in as {id}."
tiled_admins:
- provider: toy
id: alice
access_control:
access_policy: tiled.access_policies:SimpleAccessPolicy
args:
provider: toy # matches provider above
access_lists:
alice:
- A
- B
bob:
- A
- C
cara: tiled.access_policies:SimpleAccessPolicy.ALL
scopes:
- "read:metadata"
- "read:data"
public:
- D
trees:
- path: /
tree: tiled.examples.toy_authentication:tree
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 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
{"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.
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 Service Configuration Reference.
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
authentication:
secret_keys:
- "SECRET"
or by setting the TILED_SERVER_SECRET_KEYS
.
If you prefer, you can extract the keys from the environment like:
authentication:
secret_keys:
- "${SECRET}" # will be replaced by the environment variable
To rotate keys with a smooth transition, provide multiple keys
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.