OAuth2 and best practices

In web browser, only OAuth2 “implicit grant” and “authorization code” workflow can be used. Here are some interesting refs to read about them:

Discussions about recent (2019) recommendations at IETF:

These recent recommendations are to use, for SPAs, the “authorization code” workflow, with PKCE and the state parameter.

AlgoreaPlatform workflow (on 06/2019)

click "login"
click "login"
Algorea Frontend
Algorea Fron...
Algorea Platform
(Backend)
Algorea Plat...
Login Module
Login Module
credential request on /tobedefined
(with ...)
credential request on /tobedefined...
redirect to login prompt
redirect to login prompt
enter credentials, consent, ...
enter credentials, consent, ...
302 redirect to http://concours.algorea.org/login/callback_oauth.php?code=...&state=...
+ Set-Cookie: laravel_session
302 redirect to http://concours.algorea.org/login/callback_oauth.php?code=...&state=......
call
http://concours.algorea.org/login/
callback_oauth.php?code=&state=

call...
user_info
user_info
check credentials, store consent
generate authorization code
check credentials, store consent...
getAccessToken
(providing authorization code + server secret)
getAccessToken...
user info
user info
store access_token & refresh_token
associate php session to user

store access_token & refresh_tok...
access token, refresh_token,
expires_in (type: bearer)
access token, refresh_token,...
/user_api/account
(providing token)
/user_api/account(providing token)
Auth popup
Auth p...
unsafe on http!
unsafe on http!
use user_info
use user_info
any other call
Cookie: php session
any other call...
Viewer does not support full SVG 1.1

It uses a tweaked “authorization code” workflow through an authorization popup.

Some remarks:

  • The redirect from the auth module to http://concours.algorea.org leaks the authorization code on a weak (HTTP) channel. This is a violation of OAuth2.
  • The authorization on AlgoreaPlaform only relies on its PHP session after this first authorization. It means the access and refresh token provided by the login module are just ignored, including their validity time. It may lead to unconsistency between the two sessions (difficulty to revoke globally a user, problem at logout or to get user data, …). OAuth2 is not supposed to be used this way.
  • Further session refresh (and more user info fetching) can be done through the backend which has a refresh token that it can use to contact the login module again.

Possible workflows

Using “implicit” workflow

click "login"
click "login"
Algorea Frontend
Algorea Fron...
Algorea Backend
Algorea Back...
Auth Module
Auth Module
credential request on /tobedefined
(with response_type="id_token token", client_id="algoreafrontend",
redirect_uri="/redirect/uri", scope=.., state=...)
credential request on /tobedefined...
redirect to login prompt
redirect to login prompt
enter credentials, consent, ...
enter credentials, consent, ...
302 redirect to /redirect_uri#access_token=...&expires_in=7200&id_token=..&token_type=Bearer&state=
+ session cookie
302 redirect to /redirect_uri#access_token=...&expires_in=7200&id_token=..&token_type=Bearer&state=...
call /callback
with id_token
+ access_token in "authorization" headers
call /callback...
success
success
store access_token
parse id_token for user info
store access_token...
validate tokens signature
update user info from id_token
validate tokens signature...
check credentials, store consent
generate access_token (auth in JWT)
generate id token (user info in JWT)
check credentials, store consent...
any other calls
"Authorization" header: access_token
any other calls...
Viewer does not support full SVG 1.1

Some remarks:

  • The implicit workflow cannot use refresh token as it is considered unsecure to store it in the SPA. The refresh may be done through “silent authentication”, i.e., requesting the auth popup (or an iframe) to use the session with the auth server to ask for a new access token.
  • As written above, the recent (2019) official recommendation is not to use “implicit” anymore as it may leak the token too easily (through url, history, weak channel, …).

Using “authorization code “ workflow

click "login"
click "login"
Algorea Frontend
Algorea Fron...
Algorea Backend
Algorea Back...
Login Module
Login Module
credential request on /tobedefined
(with ...)
credential request on /tobedefined...
redirect to login prompt
redirect to login prompt
enter credentials, consent, ...
enter credentials, consent, ...
302 redirect to https://algorea/callback_oauth?code=...&state=...
+ session cookie
302 redirect to https://algorea/callback_oauth?code=...&state=......
https://algorea/callback_oauth?code=&state=
https://algorea/callback_oauth?code=&state=
user_info
+ access_token
user_info...
check credentials, store consent
generate authorization code
check credentials, store consent...
getAccessToken
(providing authorization code + server secret)
getAccessToken...
user info
user info
access token, refresh_token,
expires_in (type: bearer)
access token, refresh_token,...
/user_api/account
(providing access token)
/user_api/account(providing access token)
any other calls
"Authorization" header: access_token
any other calls...
Viewer does not support full SVG 1.1

Some remarks:

  • As OAuth2 defines that apps such as SPAs are not allowed to store refresh token, there is no “OAuth” way to get a new access token when it has expired. One way to proceed is to use “silent authentication” as defined above for the implicit workflow.

Current proposal

The following workflow using OAuth2 “authorization code” looks the more approriate to Algorea. Actually, the main difference with what has been done so far is that the access token is used by the frontend directly.

click "login"
click "login"
Algorea Frontend
(SPA)
Algorea Fron...
Algorea Backend
Algorea Back...
Login Module
Login Module
GET /oauth/authorize?scope=account&state=<state>&response_type=code
&code_challenge=<code_challenge>&code_challenge_method=S256
&client_id=17&locale=fr&redirect_uri=urlencoded_SPA_return_url
GET /oauth/authorize?scope=account&state=<state>&response_type=code...
redirect to login prompt
redirect to login prompt
enter credentials, consent, ...
enter credentials, consent, ...
302 redirect to https://spa_url?code=...&state=... + set session cookie
302 redirect to https://spa_url?code=...&state=... + set session cookie
POST https://backend/auth/token
with code, code_verifier
POST https://backend/auth/token...
access_token, expire_in
access_token, expire_in
POST https://login-module/oauth/token
with code, code_verifier, server secret
POST https://login-module/oauth/token...
{access token:..., refresh_token:...,
expires_in: 3600, type: bearer, scope: null }
{access token:..., refresh_token:...,...
check credentials,
store consent
generate authorization code
check credentials,...
update (or create) user
store access_token & refresh_token
update (or create) user...
store access_token
store access_token
user info
user info
GET /user_api/account
GET /user_api/account
user info
user info
GET /current-user/
GET /current-user/
Generate:
- state: a random 32-byte string (->store it)
- code_verifier**: a random 32-byte string (->store it)
- code_challenge**: sha256(code_verifier)
(all base-64 encoded with +,/,= replaced)
(** requires login-module to support PKCE)

Generate:...
update user info in UI
update user info i...
Extract code & state
Verify state
Extract code & state...
redirect
redirect
User Agent
User Agent
Viewer does not support full SVG 1.1

Refreshing tokens

Login module currently uses the default values of 1 hour for access token lifetime and 2 weeks for the refresh token.

As AlgoreaBackend cannot validate the access token provided by the frontend once it has expired, there are two strategies for keeping the user logged in (without required credentials re-entry) more than the token lifetime:

  • Anticipating the access token refresh. When the frontend know the token is only valid for 10min, it may ask the backend to get a new access token (the backend has a refresh token to do so).
  • Use “silent authentication” (which is a bit cheating with OAuth2), by re-opening the auth popup (may be in a hidden iframe), and use the session cookie used at first login to do a new authentication without prompt to the user. It requires this session to be long-enough (longer than the access token lifetime).