From 7b281f4c21d074ac3b9d6f519992cb3cd1bdf408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Thu, 1 Sep 2022 15:54:50 +0200 Subject: [PATCH] Improve docs and spec compliance of oauth2-types requests --- .../handlers/src/oauth2/authorization/mod.rs | 22 +- crates/oauth2-types/src/requests.rs | 207 +++++++++++++++++- 2 files changed, 213 insertions(+), 16 deletions(-) diff --git a/crates/handlers/src/oauth2/authorization/mod.rs b/crates/handlers/src/oauth2/authorization/mod.rs index 15fe6afe..ab713ffd 100644 --- a/crates/handlers/src/oauth2/authorization/mod.rs +++ b/crates/handlers/src/oauth2/authorization/mod.rs @@ -193,6 +193,7 @@ pub(crate) async fn get( .load_session(&mut txn) .await .context("failed to load browser session")?; + let prompt = params.auth.prompt.as_deref().unwrap_or_default(); // Check if the request/request_uri/registration params are used. If so, reply // with the right error since we don't support them. @@ -234,7 +235,7 @@ pub(crate) async fn get( } // Fail early if prompt=none and there is no active session - if params.auth.prompt == Some(Prompt::None) && maybe_session.is_none() { + if prompt.contains(&Prompt::None) && maybe_session.is_none() { return Ok(callback_destination .go( &templates, @@ -272,7 +273,7 @@ pub(crate) async fn get( None }; - let requires_consent = params.auth.prompt == Some(Prompt::Consent); + let requires_consent = prompt.contains(&Prompt::Consent); let grant = new_authorization_grant( &mut txn, @@ -292,13 +293,13 @@ pub(crate) async fn get( .await?; let continue_grant = PostAuthAction::continue_grant(grant.data); - let res = match (maybe_session, params.auth.prompt) { + let res = match maybe_session { // Cases where there is no active session, redirect to the relevant page - (None, Some(Prompt::None)) => { + None if prompt.contains(&Prompt::None) => { // This case should already be handled earlier unreachable!(); } - (None, Some(Prompt::Create)) => { + None if prompt.contains(&Prompt::Create) => { // Client asked for a registration, show the registration prompt txn.commit().await?; @@ -306,7 +307,7 @@ pub(crate) async fn get( .go() .into_response() } - (None, _) => { + None => { // Other cases where we don't have a session, ask for a login txn.commit().await?; @@ -316,7 +317,10 @@ pub(crate) async fn get( } // Special case when we already have a sesion but prompt=login|select_account - (Some(_), Some(Prompt::Login | Prompt::SelectAccount)) => { + Some(_) + if prompt.contains(&Prompt::Login) + || prompt.contains(&Prompt::SelectAccount) => + { // TODO: better pages here txn.commit().await?; @@ -326,7 +330,7 @@ pub(crate) async fn get( } // Else, we immediately try to complete the authorization grant - (Some(user_session), Some(Prompt::None)) => { + Some(user_session) if prompt.contains(&Prompt::None) => { // With prompt=none, we should get back to the client immediately match self::complete::complete(grant, user_session, &policy_factory, txn).await { @@ -362,7 +366,7 @@ pub(crate) async fn get( } } } - (Some(user_session), _) => { + Some(user_session) => { let grant_id = grant.data; // Else, we show the relevant reauth/consent page if necessary match self::complete::complete(grant, user_session, &policy_factory, txn).await diff --git a/crates/oauth2-types/src/requests.rs b/crates/oauth2-types/src/requests.rs index b8b6fe82..147afa1b 100644 --- a/crates/oauth2-types/src/requests.rs +++ b/crates/oauth2-types/src/requests.rs @@ -31,6 +31,10 @@ use crate::scope::Scope; // ref: https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml +/// The mechanism to be used for returning Authorization Response parameters +/// from the Authorization Endpoint. +/// +/// Defined in [OAuth 2.0 Multiple Response Type Encoding Practices](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes). #[derive( Debug, Hash, @@ -47,11 +51,28 @@ use crate::scope::Scope; )] #[serde(rename_all = "snake_case")] pub enum ResponseMode { + /// Authorization Response parameters are encoded in the query string added + /// to the `redirect_uri`. Query, + + /// Authorization Response parameters are encoded in the fragment added to + /// the `redirect_uri`. Fragment, + + /// Authorization Response parameters are encoded as HTML form values that + /// are auto-submitted in the User Agent, and thus are transmitted via the + /// HTTP `POST` method to the Client, with the result parameters being + /// encoded in the body using the `application/x-www-form-urlencoded` + /// format. + /// + /// Defined in [OAuth 2.0 Form Post Response Mode](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html). FormPost, } +/// Value that specifies how the Authorization Server displays the +/// authentication and consent user interface pages to the End-User. +/// +/// Defined in [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). #[derive( Debug, Hash, @@ -68,12 +89,35 @@ pub enum ResponseMode { )] #[serde(rename_all = "snake_case")] pub enum Display { + /// The Authorization Server should display the authentication and consent + /// UI consistent with a full User Agent page view. + /// + /// This is the default display mode. Page, + + /// The Authorization Server should display the authentication and consent + /// UI consistent with a popup User Agent window. Popup, + + /// The Authorization Server should display the authentication and consent + /// UI consistent with a device that leverages a touch interface. Touch, + + /// The Authorization Server should display the authentication and consent + /// UI consistent with a "feature phone" type display. Wap, } +impl Default for Display { + fn default() -> Self { + Self::Page + } +} + +/// Value that specifies whether the Authorization Server prompts the End-User +/// for reauthentication and consent. +/// +/// Defined in [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). #[derive( Debug, Hash, @@ -91,55 +135,130 @@ pub enum Display { #[display(style = "snake_case")] #[serde(rename_all = "snake_case")] pub enum Prompt { + /// The Authorization Server must not display any authentication or consent + /// user interface pages. None, + + /// The Authorization Server should prompt the End-User for + /// reauthentication. Login, + + /// The Authorization Server should prompt the End-User for consent before + /// returning information to the Client. Consent, + + /// The Authorization Server should prompt the End-User to select a user + /// account. + /// + /// This enables an End-User who has multiple accounts at the Authorization + /// Server to select amongst the multiple accounts that they might have + /// current sessions for. SelectAccount, + + /// The Authorization Server should prompt the End-User to create a user + /// account. + /// + /// Defined in [Initiating User Registration via OpenID Connect](https://openid.net/specs/openid-connect-prompt-create-1_0.html). Create, } +/// The body of a request to the [Authorization Endpoint]. +/// +/// [Authorization Endpoint]: https://www.rfc-editor.org/rfc/rfc6749.html#section-3.1 #[skip_serializing_none] #[serde_as] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct AuthorizationRequest { + /// OAuth 2.0 Response Type value that determines the authorization + /// processing flow to be used. pub response_type: OAuthAuthorizationEndpointResponseType, + /// OAuth 2.0 Client Identifier valid at the Authorization Server. pub client_id: String, + /// Redirection URI to which the response will be sent. + /// + /// This field is required when using a response type returning an + /// authorization code. + /// + /// This URI must have been pre-registered with the OpenID Provider. pub redirect_uri: Option, + /// The scope of the access request. + /// + /// OpenID Connect requests must contain the `openid` scope value. pub scope: Scope, + /// Opaque value used to maintain state between the request and the + /// callback. pub state: Option, + /// The mechanism to be used for returning parameters from the Authorization + /// Endpoint. + /// + /// This use of this parameter is not recommended when the Response Mode + /// that would be requested is the default mode specified for the Response + /// Type. pub response_mode: Option, + /// String value used to associate a Client session with an ID Token, and to + /// mitigate replay attacks. pub nonce: Option, + /// How the Authorization Server should display the authentication and + /// consent user interface pages to the End-User. pub display: Option, - pub prompt: Option, + /// Whether the Authorization Server should prompt the End-User for + /// reauthentication and consent. + /// + /// If [`Prompt::None`] is used, it must be the only value. + #[serde_as(as = "Option>")] + #[serde(default)] + pub prompt: Option>, + /// The allowable elapsed time in seconds since the last time the End-User + /// was actively authenticated by the OpenID Provider. #[serde(default)] #[serde_as(as = "Option")] pub max_age: Option, + /// End-User's preferred languages and scripts for the user interface. #[serde_as(as = "Option>")] #[serde(default)] pub ui_locales: Option>, + /// ID Token previously issued by the Authorization Server being passed as a + /// hint about the End-User's current or past authenticated session with the + /// Client. pub id_token_hint: Option, + /// Hint to the Authorization Server about the login identifier the End-User + /// might use to log in. pub login_hint: Option, + /// Requested Authentication Context Class Reference values. #[serde_as(as = "Option>")] #[serde(default)] pub acr_values: Option>, + /// A JWT that contains the request's parameter values, called a [Request + /// Object]. + /// + /// [Request Object]: https://openid.net/specs/openid-connect-core-1_0.html#RequestObject pub request: Option, + /// A URI referencing a [Request Object] or a [Pushed Authorization + /// Request]. + /// + /// [Request Object]: https://openid.net/specs/openid-connect-core-1_0.html#RequestUriParameter + /// [Pushed Authorization Request]: https://datatracker.ietf.org/doc/html/rfc9126 pub request_uri: Option, + /// A JSON object containing the Client Metadata when interacting with a + /// [Self-Issued OpenID Provider]. + /// + /// [Self-Issued OpenID Provider]: https://openid.net/specs/openid-connect-core-1_0.html#SelfIssued pub registration: Option, } @@ -173,6 +292,9 @@ impl AuthorizationRequest { } } +/// A successful response from the [Authorization Endpoint]. +/// +/// [Authorization Endpoint]: https://www.rfc-editor.org/rfc/rfc6749.html#section-3.1 #[skip_serializing_none] #[derive(Serialize, Deserialize, Default, Debug, Clone)] pub struct AuthorizationResponse { @@ -181,33 +303,58 @@ pub struct AuthorizationResponse { pub response: R, } +/// A request to the [Token Endpoint] for the [Authorization Code] grant type. +/// +/// [Token Endpoint]: https://www.rfc-editor.org/rfc/rfc6749#section-3.2 +/// [Authorization Code]: https://www.rfc-editor.org/rfc/rfc6749#section-4.1 #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct AuthorizationCodeGrant { + /// The authorization code that was returned from the authorization + /// endpoint. pub code: String, - #[serde(default)] + + /// The `redirect_uri` that was included in the authorization request. + /// + /// This field must match exactly the value passed to the authorization + /// endpoint. pub redirect_uri: Option, + /// The code verifier that matches the code challenge that was sent to the + /// authorization endpoint. // TODO: move this somehow in the pkce module - #[serde(default)] pub code_verifier: Option, } +/// A request to the [Token Endpoint] for [refreshing an access token]. +/// +/// [Token Endpoint]: https://www.rfc-editor.org/rfc/rfc6749#section-3.2 +/// [refreshing an access token]: https://www.rfc-editor.org/rfc/rfc6749#section-6 #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct RefreshTokenGrant { + /// The refresh token issued to the client. pub refresh_token: String, - #[serde(default)] + /// The scope of the access request. + /// + /// The requested scope must not include any scope not originally granted by + /// the resource owner, and if omitted is treated as equal to the scope + /// originally granted by the resource owner. pub scope: Option, } +/// A request to the [Token Endpoint] for the [Client Credentials] grant type. +/// +/// [Token Endpoint]: https://www.rfc-editor.org/rfc/rfc6749#section-3.2 +/// [Client Credentials]: https://www.rfc-editor.org/rfc/rfc6749#section-4.4 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ClientCredentialsGrant { - #[serde(default)] + /// The scope of the access request. pub scope: Option, } +/// All possible values for the `grant_type` parameter. #[derive( Debug, Hash, @@ -224,40 +371,62 @@ pub struct ClientCredentialsGrant { )] #[serde(rename_all = "snake_case")] pub enum GrantType { + /// [`authorization_code`](https://www.rfc-editor.org/rfc/rfc6749#section-4.1) AuthorizationCode, + + /// [`refresh_token`](https://www.rfc-editor.org/rfc/rfc6749#section-6) RefreshToken, + + /// [`implicit`](https://www.rfc-editor.org/rfc/rfc6749#section-4.2) Implicit, + + /// [`client_credentials`](https://www.rfc-editor.org/rfc/rfc6749#section-4.4) ClientCredentials, } +/// An enum representing the possible requests to the [Token Endpoint]. +/// +/// [Token Endpoint]: https://www.rfc-editor.org/rfc/rfc6749#section-3.2 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(tag = "grant_type", rename_all = "snake_case")] pub enum AccessTokenRequest { AuthorizationCode(AuthorizationCodeGrant), RefreshToken(RefreshTokenGrant), ClientCredentials(ClientCredentialsGrant), - #[serde(skip_deserializing, other)] + #[serde(skip, other)] Unsupported, } +/// A successful response from the [Token Endpoint]. +/// +/// [Token Endpoint]: https://www.rfc-editor.org/rfc/rfc6749#section-3.2 #[serde_as] #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct AccessTokenResponse { + /// The access token to access the requested scope. pub access_token: String, + + /// The token to refresh the access token when it expires. pub refresh_token: Option, + + /// ID Token value associated with the authenticated session. // TODO: this should be somewhere else pub id_token: Option, + /// The type of the access token. pub token_type: OAuthAccessTokenType, + /// The duration for which the access token is valid. #[serde_as(as = "Option>")] pub expires_in: Option, + /// The scope of the access token. pub scope: Option, } impl AccessTokenResponse { + /// Creates a new `AccessTokenResponse` with the given access token. #[must_use] pub fn new(access_token: String) -> AccessTokenResponse { AccessTokenResponse { @@ -270,24 +439,28 @@ impl AccessTokenResponse { } } + /// Adds a refresh token to an `AccessTokenResponse`. #[must_use] pub fn with_refresh_token(mut self, refresh_token: String) -> Self { self.refresh_token = Some(refresh_token); self } + /// Adds an ID token to an `AccessTokenResponse`. #[must_use] pub fn with_id_token(mut self, id_token: String) -> Self { self.id_token = Some(id_token); self } + /// Adds a scope to an `AccessTokenResponse`. #[must_use] pub fn with_scope(mut self, scope: Scope) -> Self { self.scope = Some(scope); self } + /// Adds an expiration duration to an `AccessTokenResponse`. #[must_use] pub fn with_expires_in(mut self, expires_in: Duration) -> Self { self.expires_in = Some(expires_in); @@ -295,44 +468,64 @@ impl AccessTokenResponse { } } +/// A request to the [Introspection Endpoint]. +/// +/// [Introspection Endpoint]: https://www.rfc-editor.org/rfc/rfc7662#section-2 #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct IntrospectionRequest { + /// The value of the token. pub token: String, - #[serde(default)] + /// A hint about the type of the token submitted for introspection. pub token_type_hint: Option, } +/// A successful response from the [Introspection Endpoint]. +/// +/// [Introspection Endpoint]: https://www.rfc-editor.org/rfc/rfc7662#section-2 #[serde_as] #[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)] pub struct IntrospectionResponse { + /// Whether or not the presented token is currently active. pub active: bool, + /// The scope associated with the token. pub scope: Option, + /// Client identifier for the OAuth 2.0 client that requested this token. pub client_id: Option, + /// Human-readable identifier for the resource owner who authorized this + /// token. pub username: Option, + /// Type of the token. pub token_type: Option, + /// Timestamp indicating when the token will expire. #[serde_as(as = "Option")] pub exp: Option>, + /// Timestamp indicating when the token was issued. #[serde_as(as = "Option")] pub iat: Option>, + /// Timestamp indicating when the token is not to be used before. #[serde_as(as = "Option")] pub nbf: Option>, + /// Subject of the token. pub sub: Option, + /// Intended audience of the token. pub aud: Option, + /// Issuer of the token. pub iss: Option, + /// String identifier for the token. pub jti: Option, }