|
| 1 | +# MSC2964: Usage of OAuth 2.0 authorization code grant and refresh token grant |
| 2 | + |
| 3 | +This proposal is part of the broader [MSC3861: Next-generation auth for Matrix, based on OAuth 2.0/OIDC][MSC3861]. |
| 4 | + |
| 5 | +This MSC in particular defines how clients can leverage the OAuth 2.0 authorization code grant to gain access to the Matrix Client-to-Server API. |
| 6 | + |
| 7 | +## Proposal |
| 8 | + |
| 9 | +### Prerequisites |
| 10 | + |
| 11 | +This proposal requires the client to know the following authorization server metadata about the homeserver: |
| 12 | + |
| 13 | +- `authorization_endpoint`: the URL where the user should be sent to initiate the login flow |
| 14 | +- `token_endpoint`: the URL where the client is able to exchange the authorization code for an access token |
| 15 | +- `response_types_supported`: a JSON array of response types supported by the authorization endpoint |
| 16 | +- `grant_types_supported`: a JSON array of grant types supported by the authorization endpoint defined in [RFC8414] and used in [RFC6749] |
| 17 | +- `response_mode_supported`: a JSON array of response modes supported by the authorization endpoint |
| 18 | + |
| 19 | +All of those metadata values are well-defined in [RFC8414] and used in various RFCs like [RFC6749]. |
| 20 | + |
| 21 | +The discovery of the above metadata is out of scope for this MSC, and is currently covered by [MSC2965](https://github.com/matrix-org/matrix-doc/pull/2965). |
| 22 | + |
| 23 | +The client must also have a `client_id` to use with this flow. |
| 24 | +How the client obtains this is out of scope for this MSC, and is currently covered by [MSC2966](https://github.com/matrix-org/matrix-doc/pull/2966). |
| 25 | + |
| 26 | +### Authorization code grant |
| 27 | + |
| 28 | +As per [RFC6749], the authorization code grant lets the client obtain an access token through a browser redirect. |
| 29 | + |
| 30 | +Because this flow has various parameters and security improvements added by other specifications, this describes what is enforced and required to support by the client and the homeserver. |
| 31 | + |
| 32 | +Homeservers and clients must: |
| 33 | + |
| 34 | +- support PKCE using the `S256` code challenge method as per [RFC7636] |
| 35 | +- support the auth code flow as per [RFC6749] section 4.1 |
| 36 | +- support the refresh token grant as per [RFC6749] section 6 |
| 37 | +- use pre-registered, strict redirect URIs |
| 38 | +- use the `fragment` response mode as per [OAuth 2.0 Multiple Response Type Encoding Practices] for clients with an HTTPS redirect URI |
| 39 | + |
| 40 | +### Refresh token grant |
| 41 | + |
| 42 | +When authorization is granted to a client, the homeserver must issue a refresh token to the client in addition to the access token. |
| 43 | + |
| 44 | +The access token must be short-lived and should be refreshed using the `refresh_token` when expired, as described in [RFC6749] section 6. |
| 45 | + |
| 46 | +The homeserver should issue a new refresh token each time one is used, and invalidate the old one. |
| 47 | +It should do this only if it can guarantee that in case a response with a new refresh token is not received and stored by the client, retrying the request with the old refresh token will succeed. |
| 48 | + |
| 49 | +The homeserver should consider that the session is compromised if an old, invalidated refresh token is being used, and should revoke the session. |
| 50 | + |
| 51 | +The client must handle access token refresh failures as follows: |
| 52 | + |
| 53 | + - If the refresh fails due to network issues or a `5xx` HTTP status code from the server, the client should retry the request with the old refresh token later. |
| 54 | + - If the refresh fails due to a `4xx` HTTP status code from the server, the client should consider the session logged out. |
| 55 | + |
| 56 | +### Sample flow |
| 57 | + |
| 58 | +#### Flow parameters |
| 59 | + |
| 60 | +The client must know the following parameters, through ways described in [MSC2965], [MSC2966] and [MSC2967]: |
| 61 | + |
| 62 | +- `authorization_endpoint`: the URL where the user is able to access the authorization endpoint to initiate the login flow |
| 63 | +- `token_endpoint`: the URL where the user is able to access the token endpoint to exchange the authorization code for an access token |
| 64 | +- `client_id`: the unique identifier allocated for the client |
| 65 | +- `redirect_uri`: the URI where the user is redirected after the authorization flow used by this client |
| 66 | +- `scope`: the scope of the access token to request |
| 67 | +- `response_mode`: the response mode to use, either `fragment` or `query`. It must be `fragment` if the `redirect_uri` is an HTTPS URI, and can be `query` otherwise |
| 68 | + |
| 69 | +It needs to generate the following values: |
| 70 | + |
| 71 | +- a random value for the `state` |
| 72 | +- a cryptographically random value for the `code_verifier` |
| 73 | + |
| 74 | +#### Authorization request |
| 75 | + |
| 76 | +It then constructs the authorization request URL using the `authorization_endpoint` value, with the following query parameters: |
| 77 | + |
| 78 | +- The `response_type` value set to `code` |
| 79 | +- The `client_id` value |
| 80 | +- The `redirect_uri` value |
| 81 | +- The `scope` value |
| 82 | +- The `state` value |
| 83 | +- The `response_mode` value |
| 84 | +- The `code_challenge` computed from the `code_verifier` value using the SHA-256 algorithm, as described in [RFC7636] |
| 85 | +- The `code_challenge_method` set to `S256` |
| 86 | + |
| 87 | +This authorization request URL must be opened in the user's browser: |
| 88 | + |
| 89 | +- For web-based clients, this can be done through a redirection or by opening the URL in a new tab |
| 90 | +- For native clients, this can be done by opening the URL: |
| 91 | + - using the system browser |
| 92 | + - through platform-specific APIs when available, such as [`ASWebAuthenticationSession`](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession) on iOS or [Android Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs) on Android |
| 93 | + |
| 94 | +The rationale for using the system browser is explained in [MSC3861], under "Motivation" → "Benefits of authenticating end-users through the system browser". |
| 95 | + |
| 96 | +##### Sample authorization request |
| 97 | + |
| 98 | +Sample authorization request (broken down into multiple lines for readability), with the following values: |
| 99 | + |
| 100 | +- `authorization_endpoint` set to `https://account.example.com/oauth2/auth`, obtained through [MSC2965] |
| 101 | +- `client_id` set to `s6BhdRkqt3`, obtained through [MSC2966] |
| 102 | +- `redirect_uri` set to `https://app.example.com/oauth2-callback` |
| 103 | +- `state` set to `ewubooN9weezeewah9fol4oothohroh3` |
| 104 | +- `response_mode` set to `fragment` |
| 105 | +- `code_verifier` set to `ogie4iVaeteeKeeLaid0aizuimairaCh` |
| 106 | +- `code_challenge` computed as `72xySjpngTcCxgbPfFmkPHjMvVDl2jW1aWP7-J6rmwU` |
| 107 | +- `scope` set to `urn:matrix:client:api:* urn:matrix:client:device:AAABBBCCCDDD` (full access to the C-S API, using the `AAABBBCCCDDD` device ID, as per [MSC2967]) |
| 108 | + |
| 109 | +``` |
| 110 | +https://account.example.com/oauth2/auth? |
| 111 | + client_id = s6BhdRkqt3 & |
| 112 | + response_type = code & |
| 113 | + response_mode = fragment & |
| 114 | + redirect_uri = https://app.example.com/oauth2-callback & |
| 115 | + scope = urn:matrix:client:api:* urn:matrix:client:device:AAABBBCCCDDD & |
| 116 | + state = ewubooN9weezeewah9fol4oothohroh3 & |
| 117 | + code_challenge = 72xySjpngTcCxgbPfFmkPHjMvVDl2jW1aWP7-J6rmwU & |
| 118 | + code_challenge_method = S256 |
| 119 | +``` |
| 120 | + |
| 121 | +#### Callback |
| 122 | + |
| 123 | +Once completed, the user is redirected to the `redirect_uri`, with either a successful or failed authorization in the URL fragment or query parameters. |
| 124 | +Whether the parameters are in the URL fragment or query parameters is determined by the `response_mode` value: |
| 125 | + |
| 126 | +- if set to `fragment`, the parameters will be placed in the URL fragment, like `https://example.com/callback#param1=value1¶m2=value2` |
| 127 | +- if set to `query`, the parameters will be in placed the query string, like `com.example.app:/callback?param1=value1¶m2=value2` |
| 128 | + |
| 129 | +To avoid disclosing the parameters to the web server hosting the `redirect_uri`, clients should use the `fragment` response mode if the `redirect_uri` is an HTTP/HTTPS URI with a remote host. |
| 130 | + |
| 131 | +In both success and failure cases, the parameters will have the `state` value used in the authorization request. |
| 132 | + |
| 133 | +##### Successful authorization callback |
| 134 | + |
| 135 | +Successful authorization will have a `code` value. |
| 136 | + |
| 137 | +Sample successful authorization: |
| 138 | + |
| 139 | +``` |
| 140 | +https://app.example.com/oauth2-callback#state=ewubooN9weezeewah9fol4oothohroh3&code=iuB7Eiz9heengah1joh2ioy9ahChuP6R |
| 141 | +``` |
| 142 | + |
| 143 | +##### Failed authorization callback |
| 144 | + |
| 145 | +Failed authorization will have the following values: |
| 146 | + |
| 147 | +- `error`: the error code |
| 148 | +- `error_description`: the error description (optional) |
| 149 | +- `error_uri`: the URI where the user can find more information about the error (optional) |
| 150 | + |
| 151 | +Sample failed authorization: |
| 152 | + |
| 153 | +``` |
| 154 | +https://app.example.com/oauth2-callback#state=ewubooN9weezeewah9fol4oothohroh3&error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request.&error_uri=https%3A%2F%2Ferrors.example.com%2F |
| 155 | +``` |
| 156 | + |
| 157 | +#### Token request |
| 158 | + |
| 159 | +The client then exchanges the authorization code to obtain an access token using the token endpoint. |
| 160 | + |
| 161 | +This is done by making a POST request to the `token_endpoint` with the following parameters, encoded as `application/x-www-form-urlencoded` in the body: |
| 162 | + |
| 163 | +- The `grant_type` set to `authorization_code` |
| 164 | +- The `code` obtained from the callback |
| 165 | +- The `redirect_uri` used in the authorization request |
| 166 | +- The `client_id` value |
| 167 | +- The `code_verifier` value generated at the start of the authorization flow |
| 168 | + |
| 169 | +The server replies with a JSON object containing the access token, the token type, the expiration time, and the refresh token. |
| 170 | + |
| 171 | +The access token must be short-lived and should be refreshed using the `refresh_token` when expired. |
| 172 | + |
| 173 | +##### Sample token request |
| 174 | + |
| 175 | +``` |
| 176 | +POST /oauth2/token HTTP/1.1 |
| 177 | +Host: account.example.com |
| 178 | +Content-Type: application/x-www-form-urlencoded |
| 179 | +Accept: application/json |
| 180 | +
|
| 181 | +grant_type=authorization_code |
| 182 | + &code=iuB7Eiz9heengah1joh2ioy9ahChuP6R |
| 183 | + &redirect_uri=https://app.example.com/oauth2-callback |
| 184 | + &client_id=s6BhdRkqt3 |
| 185 | + &code_verifier=ogie4iVaeteeKeeLaid0aizuimairaCh |
| 186 | +``` |
| 187 | + |
| 188 | +```json |
| 189 | +{ |
| 190 | + "access_token": "2YotnFZFEjr1zCsicMWpAA", |
| 191 | + "token_type": "Bearer", |
| 192 | + "expires_in": 299, |
| 193 | + "refresh_token": "tGz3JOkF0XG5Qx2TlKWIA", |
| 194 | + "scope": "urn:matrix:client:api:* urn:matrix:client:device:AAABBBCCCDDD" |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +#### Token refresh |
| 199 | + |
| 200 | +When the access token expires, the client must refresh it by making a POST request to the `token_endpoint` with the following parameters, encoded as `application/x-www-form-urlencoded` in the body: |
| 201 | + |
| 202 | +- The `grant_type` set to `refresh_token` |
| 203 | +- The `refresh_token` obtained from the token response |
| 204 | +- The `client_id` value |
| 205 | + |
| 206 | +The server replies with a JSON object containing the new access token, the token type, the expiration time, and a new refresh token. |
| 207 | +The old refresh token is no longer valid and should be discarded. |
| 208 | + |
| 209 | +##### Sample token refresh |
| 210 | + |
| 211 | +``` |
| 212 | +POST /oauth2/token HTTP/1.1 |
| 213 | +Host: account.example.com |
| 214 | +Content-Type: application/x-www-form-urlencoded |
| 215 | +Accept: application/json |
| 216 | +
|
| 217 | +grant_type=refresh_token |
| 218 | + &refresh_token=tGz3JOkF0XG5Qx2TlKWIA |
| 219 | + &client_id=s6BhdRkqt3 |
| 220 | +``` |
| 221 | + |
| 222 | +```json |
| 223 | +{ |
| 224 | + "access_token": "2YotnFZFEjr1zCsicMWpAA", |
| 225 | + "token_type": "Bearer", |
| 226 | + "expires_in": 299, |
| 227 | + "refresh_token": "tGz3JOkF0XG5Qx2TlKWIA", |
| 228 | + "scope": "urn:matrix:client:api:* urn:matrix:client:device:AAABBBCCCDDD" |
| 229 | +} |
| 230 | +``` |
| 231 | + |
| 232 | +### User registration |
| 233 | + |
| 234 | +Users can register themselves by initiating an authorization code flow with the `prompt=create` parameter as defined in [Initiating User Registration via OpenID Connect 1.0](https://openid.net/specs/openid-connect-prompt-create-1_0.html). |
| 235 | + |
| 236 | +Whether the homeserver supports this parameter is advertised by the `prompt_values_supported` authorization server metadata. |
| 237 | + |
| 238 | +## Potential issues |
| 239 | + |
| 240 | +For a discussion on potential issues please see [MSC3861] |
| 241 | + |
| 242 | +## Alternatives |
| 243 | + |
| 244 | +The authorization flow could make use of [RFC9126: OAuth 2.0 Pushed Authorization Request][RFC9126] as a way to future-proof the flow. |
| 245 | +This could help with granting very specific permissions to the client in combination with [RFC9396: OAuth 2.0 Rich Authorization Requests][RFC9396]. |
| 246 | + |
| 247 | +As Matrix clients are 'public clients' in the sense of [RFC6749] section 2.1, this proposal would not benefit from the security aspects of [RFC9126]. |
| 248 | +It could, although, give better feedback to clients when they are trying to start an invalid or unauthorized flow. |
| 249 | + |
| 250 | +Other alternatives for the global proposal are discussed in [MSC3861]. |
| 251 | + |
| 252 | +## Security considerations |
| 253 | + |
| 254 | +Since this touches one of the most sensitive parts of the API, there are a lot of security considerations to keep in mind. |
| 255 | + |
| 256 | +The [OAuth 2.0 Security Best Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-16) IETF draft outlines many potential attack scenarios. Many of these scenarios are mitigated by the choices enforced in the client profiles outlined in this MSC. |
| 257 | +It motivates the following decisions in this profile: |
| 258 | + |
| 259 | + - Using strict redirect URIs validation helps mitigate the risk of open redirection attacks. |
| 260 | + - Using the `code` response mode, alongside PKCE mitigates the risk in cases of redirection hijacking. |
| 261 | + - Usage of short-lived access tokens, along with rotation of refresh tokens mitigates the impact of leaked tokens. |
| 262 | + - Using the system browser to authenticate users lowers the risk of credentials exfiltration by the client. |
| 263 | + |
| 264 | +## Unstable prefix |
| 265 | + |
| 266 | +None as part of this MSC. |
| 267 | + |
| 268 | +## Dependencies |
| 269 | + |
| 270 | +- [MSC2965] |
| 271 | +- [MSC2966] |
| 272 | +- [MSC2967] |
| 273 | + |
| 274 | +[RFC6749]: https://tools.ietf.org/html/rfc6749 |
| 275 | +[RFC7636]: https://tools.ietf.org/html/rfc7636 |
| 276 | +[RFC8414]: https://tools.ietf.org/html/rfc8414 |
| 277 | +[RFC9126]: https://tools.ietf.org/html/rfc9126 |
| 278 | +[RFC9396]: https://tools.ietf.org/html/rfc9396 |
| 279 | +[MSC2965]: https://github.com/matrix-org/matrix-spec-proposals/pull/2965 |
| 280 | +[MSC2966]: https://github.com/matrix-org/matrix-spec-proposals/pull/2966 |
| 281 | +[MSC2967]: https://github.com/matrix-org/matrix-spec-proposals/pull/2967 |
| 282 | +[MSC3861]: https://github.com/matrix-org/matrix-spec-proposals/pull/3861 |
| 283 | +[OAuth 2.0 Multiple Response Type Encoding Practices]: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html |
0 commit comments