Ueberauth strategy for authenticating athletes with intervals.icu.
Setup
Register an application at https://intervals.icu/settings to obtain a client id and secret, and register your exact callback URL. intervals.icu does not support wildcards in redirect URIs. Every callback URL must be registered in full.
Add the provider to your Ueberauth configuration:
config :ueberauth, Ueberauth,
providers: [
intervals_icu: {Ueberauth.Strategy.IntervalsIcu, [default_scope: "ACTIVITY:READ"]}
]
config :ueberauth, Ueberauth.Strategy.IntervalsIcu.OAuth,
client_id: System.get_env("INTERVALS_ICU_CLIENT_ID"),
client_secret: System.get_env("INTERVALS_ICU_CLIENT_SECRET")Options
:default_scope- scopes requested when the request phase does not receive ascopequery parameter. Defaults to"ACTIVITY:READ,WELLNESS:READ".:fetch_athlete- whether to call the athlete endpoint after the token exchange to build a fullerUeberauth.Auth.Info. Defaults totrue. See "Fetching the athlete" below.:userinfo_endpoint- the endpoint used when:fetch_athleteis true. Defaults to"/api/v1/athlete/0". The athlete id0always means "the athlete this token belongs to".:uid_field- which athlete field becomesUeberauth.Auth.uid. Defaults to:id.:oauth2_module- the module implementing the OAuth calls. Defaults toUeberauth.Strategy.IntervalsIcu.OAuth.
Scopes
intervals.icu scopes are ACTIVITY, WELLNESS, CALENDAR, CHATS,
LIBRARY and SETTINGS, each suffixed with :READ or :WRITE, and joined
with commas rather than the spaces used by most OAuth 2.0 providers:
default_scope: "ACTIVITY:READ,WELLNESS:WRITE"Fetching the athlete
The token response already contains the athlete's id and name, so this strategy can identify the athlete without any further request.
By default it still calls :userinfo_endpoint to populate a richer
Ueberauth.Auth.Info, mirroring how most Ueberauth strategies behave. That
endpoint may require a scope your application did not request, in which case
intervals.icu answers 403 and authentication fails.
If that happens, either request the necessary scope or turn the call off:
providers: [
intervals_icu: {Ueberauth.Strategy.IntervalsIcu, [fetch_athlete: false]}
]With fetch_athlete: false no extra request is made and the auth struct is
built from the athlete map inside the token response. You get uid and
name, but not email or the other profile fields.
Tokens do not expire, and there are no refresh tokens
intervals.icu issues no refresh token and no expiry, so
Ueberauth.Auth.Credentials always comes back with refresh_token: nil,
expires: false and expires_at: nil. Store the access token and use it
until it stops working; to recover, send the athlete through the flow again.
A token can be revoked with:
DELETE https://intervals.icu/api/v1/disconnect-app
Authorization: Bearer <token>
Summary
Functions
The token, its type and its granted scopes.
Callback implementation for Ueberauth.Strategy.default_options/0.
The raw token and athlete payloads, for anything this strategy does not map.
Handles the redirect back from intervals.icu.
Removes the data this strategy stored on the connection.
Redirects the athlete to the intervals.icu authorize endpoint.
The athlete's profile information.
The athlete's unique id, taken from the field named by :uid_field.
Functions
The token, its type and its granted scopes.
refresh_token, expires and expires_at are always nil/false, because
intervals.icu issues neither refresh tokens nor expiring access tokens.
Callback implementation for Ueberauth.Strategy.default_options/0.
The raw token and athlete payloads, for anything this strategy does not map.
Handles the redirect back from intervals.icu.
On success the token is exchanged and, unless :fetch_athlete is disabled,
the athlete is fetched. A denial arrives as ?error=access_denied.
Removes the data this strategy stored on the connection.
Redirects the athlete to the intervals.icu authorize endpoint.
A scope query parameter on the request overrides :default_scope, letting
a single provider ask for different scopes per request.
The athlete's profile information.
Only :name is guaranteed, because it is present in the token response. The
remaining fields depend on what the athlete endpoint returns and are nil
when absent. The complete, untouched athlete map is always available via
extra/1.
The athlete's unique id, taken from the field named by :uid_field.