mirror of
https://github.com/girlbossceo/ruwuma.git
synced 2025-04-29 06:49:48 -04:00
client-api: Add support for authorization server metadata endpoint
According to the latest draft of MSC2965.
This commit is contained in:
parent
2953f51991
commit
bed66a04d9
5 changed files with 899 additions and 0 deletions
|
@ -6,6 +6,14 @@ Breaking changes:
|
|||
- `get_supported_versions::Response::known_versions()` returns a
|
||||
`BTreeSet<MatrixVersion>` instead of a `DoubleEndedIterator`.
|
||||
|
||||
=======
|
||||
Improvements:
|
||||
|
||||
- Add support for the authorization server metadata endpoint, according to the
|
||||
latest draft of MSC2965.
|
||||
|
||||
# 0.20.1
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- `unstable-msc4186` without `unstable-msc3575` no longer create a compilation
|
||||
|
|
|
@ -4,5 +4,7 @@ pub mod discover_homeserver;
|
|||
pub mod discover_support;
|
||||
#[cfg(feature = "unstable-msc2965")]
|
||||
pub mod get_authentication_issuer;
|
||||
#[cfg(feature = "unstable-msc2965")]
|
||||
pub mod get_authorization_server_metadata;
|
||||
pub mod get_capabilities;
|
||||
pub mod get_supported_versions;
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
//! `GET /_matrix/client/*/auth_issuer`
|
||||
//!
|
||||
//! Get the OpenID Connect Provider that is trusted by the homeserver.
|
||||
//!
|
||||
//! This endpoint has been replaced by [`get_authorization_server_metadata`] in [MSC2965].
|
||||
//!
|
||||
//! [`get_authorization_server_metadata`]: super::get_authorization_server_metadata
|
||||
//! [MSC2965]: https://github.com/matrix-org/matrix-spec-proposals/pull/2965
|
||||
|
||||
pub mod msc2965 {
|
||||
//! `MSC2965` ([MSC])
|
||||
|
@ -24,6 +29,7 @@ pub mod msc2965 {
|
|||
/// Request type for the `auth_issuer` endpoint.
|
||||
#[request(error = crate::Error)]
|
||||
#[derive(Default)]
|
||||
#[deprecated = "Replaced by the get_authorization_server_metadata endpoint."]
|
||||
pub struct Request {}
|
||||
|
||||
/// Request type for the `auth_issuer` endpoint.
|
||||
|
@ -33,6 +39,7 @@ pub mod msc2965 {
|
|||
pub issuer: String,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl Request {
|
||||
/// Creates a new empty `Request`.
|
||||
pub fn new() -> Self {
|
||||
|
|
|
@ -0,0 +1,479 @@
|
|||
//! `GET /_matrix/client/*/auth_metadata`
|
||||
//!
|
||||
//! Get the metadata of the authorization server that is trusted by the homeserver.
|
||||
|
||||
mod serde;
|
||||
|
||||
pub mod msc2965 {
|
||||
//! `MSC2965` ([MSC])
|
||||
//!
|
||||
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/2965
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use ruma_common::{
|
||||
api::{request, response, Metadata},
|
||||
metadata,
|
||||
serde::{OrdAsRefStr, PartialEqAsRefStr, PartialOrdAsRefStr, Raw, StringEnum},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::PrivOwnedStr;
|
||||
|
||||
const METADATA: Metadata = metadata! {
|
||||
method: GET,
|
||||
rate_limited: false,
|
||||
authentication: None,
|
||||
history: {
|
||||
unstable => "/_matrix/client/unstable/org.matrix.msc2965/auth_metadata",
|
||||
}
|
||||
};
|
||||
|
||||
/// Request type for the `auth_metadata` endpoint.
|
||||
#[request(error = crate::Error)]
|
||||
#[derive(Default)]
|
||||
pub struct Request {}
|
||||
|
||||
/// Request type for the `auth_metadata` endpoint.
|
||||
#[response(error = crate::Error)]
|
||||
pub struct Response {
|
||||
/// The authorization server metadata as defined in [RFC8414].
|
||||
///
|
||||
/// [RFC8414]: https://datatracker.ietf.org/doc/html/rfc8414
|
||||
#[ruma_api(body)]
|
||||
pub metadata: Raw<AuthorizationServerMetadata>,
|
||||
}
|
||||
|
||||
impl Request {
|
||||
/// Creates a new empty `Request`.
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates a new `Response` with the given serialized authorization server metadata.
|
||||
pub fn new(metadata: Raw<AuthorizationServerMetadata>) -> Self {
|
||||
Self { metadata }
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata describing the configuration of the authorization server.
|
||||
///
|
||||
/// While the metadata properties and their values are declared for OAuth 2.0 in [RFC8414] and
|
||||
/// other RFCs, this type only supports properties and values that are used for Matrix, as
|
||||
/// specified in [MSC3861] and its dependencies.
|
||||
///
|
||||
/// This type is validated to have at least all the required values during deserialization. The
|
||||
/// URLs are not validated during deserialization, to validate them use
|
||||
/// [`AuthorizationServerMetadata::validate_urls()`] or
|
||||
/// [`AuthorizationServerMetadata::insecure_validate_urls()`].
|
||||
///
|
||||
/// This type has no constructor, it should be sent as raw JSON directly.
|
||||
///
|
||||
/// [RFC8414]: https://datatracker.ietf.org/doc/html/rfc8414
|
||||
/// [MSC3861]: https://github.com/matrix-org/matrix-spec-proposals/pull/3861
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub struct AuthorizationServerMetadata {
|
||||
/// The authorization server's issuer identifier.
|
||||
///
|
||||
/// This should be a URL with no query or fragment components.
|
||||
pub issuer: Url,
|
||||
|
||||
/// URL of the authorization server's authorization endpoint ([RFC6749]).
|
||||
///
|
||||
/// [RFC6749]: https://datatracker.ietf.org/doc/html/rfc6749
|
||||
pub authorization_endpoint: Url,
|
||||
|
||||
/// URL of the authorization server's token endpoint ([RFC6749]).
|
||||
///
|
||||
/// [RFC6749]: https://datatracker.ietf.org/doc/html/rfc6749
|
||||
pub token_endpoint: Url,
|
||||
|
||||
/// URL of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint
|
||||
/// ([RFC7591]).
|
||||
///
|
||||
/// [RFC7591]: https://datatracker.ietf.org/doc/html/rfc7591
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub registration_endpoint: Option<Url>,
|
||||
|
||||
/// List of the OAuth 2.0 `response_type` values that this authorization server supports.
|
||||
///
|
||||
/// Those values are the same as those used with the `response_types` parameter defined by
|
||||
/// OAuth 2.0 Dynamic Client Registration ([RFC7591]).
|
||||
///
|
||||
/// This field must include [`ResponseType::Code`].
|
||||
///
|
||||
/// [RFC7591]: https://datatracker.ietf.org/doc/html/rfc7591
|
||||
pub response_types_supported: BTreeSet<ResponseType>,
|
||||
|
||||
/// List of the OAuth 2.0 `response_mode` values that this authorization server supports.
|
||||
///
|
||||
/// Those values are specified in [OAuth 2.0 Multiple Response Type Encoding Practices].
|
||||
///
|
||||
/// This field must include [`ResponseMode::Query`] and [`ResponseMode::Fragment`].
|
||||
///
|
||||
/// [OAuth 2.0 Multiple Response Type Encoding Practices]: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html
|
||||
pub response_modes_supported: BTreeSet<ResponseMode>,
|
||||
|
||||
/// List of the OAuth 2.0 `grant_type` values that this authorization server supports.
|
||||
///
|
||||
/// Those values are the same as those used with the `grant_types` parameter defined by
|
||||
/// OAuth 2.0 Dynamic Client Registration ([RFC7591]).
|
||||
///
|
||||
/// This field must include [`GrantType::AuthorizationCode`] and
|
||||
/// [`GrantType::RefreshToken`].
|
||||
///
|
||||
/// [RFC7591]: https://datatracker.ietf.org/doc/html/rfc7591
|
||||
pub grant_types_supported: BTreeSet<GrantType>,
|
||||
|
||||
/// URL of the authorization server's OAuth 2.0 revocation endpoint ([RFC7009]).
|
||||
///
|
||||
/// [RFC7009]: https://datatracker.ietf.org/doc/html/rfc7009
|
||||
pub revocation_endpoint: Url,
|
||||
|
||||
/// List of Proof Key for Code Exchange (PKCE) code challenge methods supported by this
|
||||
/// authorization server ([RFC7636]).
|
||||
///
|
||||
/// This field must include [`CodeChallengeMethod::S256`].
|
||||
///
|
||||
/// [RFC7636]: https://datatracker.ietf.org/doc/html/rfc7636
|
||||
pub code_challenge_methods_supported: BTreeSet<CodeChallengeMethod>,
|
||||
|
||||
/// URL where the user is able to access the account management capabilities of the
|
||||
/// authorization server ([MSC4191]).
|
||||
///
|
||||
/// [MSC4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub account_management_uri: Option<Url>,
|
||||
|
||||
/// List of actions that the account management URL supports ([MSC4191]).
|
||||
///
|
||||
/// [MSC4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
|
||||
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
|
||||
pub account_management_actions_supported: BTreeSet<AccountManagementAction>,
|
||||
|
||||
/// URL of the authorization server's device authorization endpoint ([RFC8628]).
|
||||
///
|
||||
/// [RFC8628]: https://datatracker.ietf.org/doc/html/rfc8628
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub device_authorization_endpoint: Option<Url>,
|
||||
|
||||
/// The [`Prompt`] values supported by the authorization server ([Initiating User
|
||||
/// Registration via OpenID Connect 1.0]).
|
||||
///
|
||||
/// [Initiating User Registration via OpenID Connect 1.0]: https://openid.net/specs/openid-connect-prompt-create-1_0.html
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub prompt_values_supported: Vec<Prompt>,
|
||||
}
|
||||
|
||||
impl AuthorizationServerMetadata {
|
||||
/// Strict validation of the URLs in this `AuthorizationServerMetadata`.
|
||||
///
|
||||
/// This checks that:
|
||||
///
|
||||
/// * The `issuer` is a valid URL using an `https` scheme and without a query or fragment.
|
||||
///
|
||||
/// * All the URLs use an `https` scheme.
|
||||
pub fn validate_urls(&self) -> Result<(), AuthorizationServerMetadataUrlError> {
|
||||
self.validate_urls_inner(false)
|
||||
}
|
||||
|
||||
/// Weak validation the URLs `AuthorizationServerMetadata` are all absolute URLs.
|
||||
///
|
||||
/// This only checks that the `issuer` is a valid URL without a query or fragment.
|
||||
///
|
||||
/// In production, you should prefer [`AuthorizationServerMetadata`] that also check if the
|
||||
/// URLs use an `https` scheme. This method is meant for development purposes, when
|
||||
/// interacting with a local authorization server.
|
||||
pub fn insecure_validate_urls(&self) -> Result<(), AuthorizationServerMetadataUrlError> {
|
||||
self.validate_urls_inner(true)
|
||||
}
|
||||
|
||||
/// Get an iterator over the URLs of this `AuthorizationServerMetadata`, except the
|
||||
/// `issuer`.
|
||||
fn validate_urls_inner(
|
||||
&self,
|
||||
insecure: bool,
|
||||
) -> Result<(), AuthorizationServerMetadataUrlError> {
|
||||
if self.issuer.query().is_some() || self.issuer.fragment().is_some() {
|
||||
return Err(AuthorizationServerMetadataUrlError::IssuerHasQueryOrFragment);
|
||||
}
|
||||
|
||||
if insecure {
|
||||
// No more checks.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let required_urls = &[
|
||||
("issuer", &self.issuer),
|
||||
("authorization_endpoint", &self.authorization_endpoint),
|
||||
("token_endpoint", &self.token_endpoint),
|
||||
("revocation_endpoint", &self.revocation_endpoint),
|
||||
];
|
||||
let optional_urls = &[
|
||||
self.registration_endpoint.as_ref().map(|string| ("registration_endpoint", string)),
|
||||
self.account_management_uri
|
||||
.as_ref()
|
||||
.map(|string| ("account_management_uri", string)),
|
||||
self.device_authorization_endpoint
|
||||
.as_ref()
|
||||
.map(|string| ("device_authorization_endpoint", string)),
|
||||
];
|
||||
|
||||
for (field, url) in required_urls.iter().chain(optional_urls.iter().flatten()) {
|
||||
if url.scheme() != "https" {
|
||||
return Err(AuthorizationServerMetadataUrlError::NotHttpsScheme(field));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The method to use at the authorization endpoint.
|
||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||
#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
|
||||
#[ruma_enum(rename_all = "lowercase")]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum ResponseType {
|
||||
/// Use the authorization code grant flow ([RFC6749]).
|
||||
///
|
||||
/// [RFC6749]: https://datatracker.ietf.org/doc/html/rfc6749
|
||||
Code,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
/// The mechanism to be used for returning authorization response parameters from the
|
||||
/// authorization endpoint.
|
||||
///
|
||||
/// The values are specified in [OAuth 2.0 Multiple Response Type Encoding Practices].
|
||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||
///
|
||||
/// [OAuth 2.0 Multiple Response Type Encoding Practices]: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html
|
||||
#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
|
||||
#[ruma_enum(rename_all = "lowercase")]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum ResponseMode {
|
||||
/// Authorization Response parameters are encoded in the fragment added to the
|
||||
/// `redirect_uri` when redirecting back to the client.
|
||||
Query,
|
||||
|
||||
/// Authorization Response parameters are encoded in the query string added to the
|
||||
/// `redirect_uri` when redirecting back to the client.
|
||||
Fragment,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
/// The grant type to use at the token endpoint.
|
||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||
#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
|
||||
#[ruma_enum(rename_all = "snake_case")]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum GrantType {
|
||||
/// The authorization code grant type ([RFC6749]).
|
||||
///
|
||||
/// [RFC6749]: https://datatracker.ietf.org/doc/html/rfc6749
|
||||
AuthorizationCode,
|
||||
|
||||
/// The refresh token grant type ([RFC6749]).
|
||||
///
|
||||
/// [RFC6749]: https://datatracker.ietf.org/doc/html/rfc6749
|
||||
RefreshToken,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
/// The code challenge method to use at the authorization endpoint.
|
||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||
#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
|
||||
#[ruma_enum(rename_all = "lowercase")]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum CodeChallengeMethod {
|
||||
/// Use a SHA-256, base64url-encoded code challenge ([RFC7636]).
|
||||
///
|
||||
/// [RFC7636]: https://datatracker.ietf.org/doc/html/rfc7636
|
||||
S256,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
/// The action that the user wishes to do at the account management URL.
|
||||
///
|
||||
/// The values are specified in [MSC4191].
|
||||
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
|
||||
///
|
||||
/// [MSC4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
|
||||
#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum AccountManagementAction {
|
||||
/// The user wishes to view their profile (name, avatar, contact details).
|
||||
///
|
||||
/// [RFC7636]: https://datatracker.ietf.org/doc/html/rfc7636
|
||||
#[ruma_enum(rename = "org.matrix.profile")]
|
||||
Profile,
|
||||
|
||||
/// The user wishes to view a list of their sessions.
|
||||
#[ruma_enum(rename = "org.matrix.sessions_list")]
|
||||
SessionsList,
|
||||
|
||||
/// The user wishes to view the details of a specific session.
|
||||
#[ruma_enum(rename = "org.matrix.session_view")]
|
||||
SessionView,
|
||||
|
||||
/// The user wishes to end/logout a specific session.
|
||||
#[ruma_enum(rename = "org.matrix.session_end")]
|
||||
SessionEnd,
|
||||
|
||||
/// The user wishes to deactivate their account.
|
||||
#[ruma_enum(rename = "org.matrix.account_deactivate")]
|
||||
AccountDeactivate,
|
||||
|
||||
/// The user wishes to reset their cross-signing keys.
|
||||
#[ruma_enum(rename = "org.matrix.cross_signing_reset")]
|
||||
CrossSigningReset,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
|
||||
/// The possible errors when validating URLs of [`AuthorizationServerMetadata`].
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum AuthorizationServerMetadataUrlError {
|
||||
/// The URL of the field does not use the `https` scheme.
|
||||
#[error("URL in `{0}` must use the `https` scheme")]
|
||||
NotHttpsScheme(&'static str),
|
||||
|
||||
/// The `issuer` URL has a query or fragment component.
|
||||
#[error("URL in `issuer` cannot have a query or fragment component")]
|
||||
IssuerHasQueryOrFragment,
|
||||
}
|
||||
|
||||
/// The desired user experience when using the authorization endpoint.
|
||||
#[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
|
||||
#[ruma_enum(rename_all = "lowercase")]
|
||||
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
|
||||
pub enum Prompt {
|
||||
/// The user wants to create a new account ([Initiating User Registration via OpenID
|
||||
/// Connect 1.0]).
|
||||
///
|
||||
/// [Initiating User Registration via OpenID Connect 1.0]: https://openid.net/specs/openid-connect-prompt-create-1_0.html
|
||||
Create,
|
||||
|
||||
#[doc(hidden)]
|
||||
_Custom(PrivOwnedStr),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{from_value as from_json_value, json, Value as JsonValue};
|
||||
use url::Url;
|
||||
|
||||
use super::msc2965::AuthorizationServerMetadata;
|
||||
|
||||
/// A valid `AuthorizationServerMetadata` with all fields and values, as a JSON object.
|
||||
pub(super) fn authorization_server_metadata_json() -> JsonValue {
|
||||
json!({
|
||||
"issuer": "https://server.local/",
|
||||
"authorization_endpoint": "https://server.local/authorize",
|
||||
"token_endpoint": "https://server.local/token",
|
||||
"registration_endpoint": "https://server.local/register",
|
||||
"response_types_supported": ["code"],
|
||||
"response_modes_supported": ["query", "fragment"],
|
||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||
"revocation_endpoint": "https://server.local/revoke",
|
||||
"code_challenge_methods_supported": ["s256"],
|
||||
"account_management_uri": "https://server.local/account",
|
||||
"account_management_actions_supported": [
|
||||
"org.matrix.profile",
|
||||
"org.matrix.sessions_list",
|
||||
"org.matrix.session_view",
|
||||
"org.matrix.session_end",
|
||||
"org.matrix.account_deactivate",
|
||||
"org.matrix.cross_signing_reset",
|
||||
],
|
||||
"device_authorization_endpoint": "https://server.local/device",
|
||||
})
|
||||
}
|
||||
|
||||
/// A valid `AuthorizationServerMetadata`, with valid URLs.
|
||||
fn authorization_server_metadata() -> AuthorizationServerMetadata {
|
||||
from_json_value(authorization_server_metadata_json()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_valid_urls() {
|
||||
let metadata = authorization_server_metadata();
|
||||
metadata.validate_urls().unwrap();
|
||||
metadata.insecure_validate_urls().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_invalid_or_insecure_issuer() {
|
||||
let original_metadata = authorization_server_metadata();
|
||||
|
||||
// URL with query string.
|
||||
let mut metadata = original_metadata.clone();
|
||||
metadata.issuer = Url::parse("https://server.local/?session=1er45elp").unwrap();
|
||||
metadata.validate_urls().unwrap_err();
|
||||
metadata.insecure_validate_urls().unwrap_err();
|
||||
|
||||
// URL with fragment.
|
||||
let mut metadata = original_metadata.clone();
|
||||
metadata.issuer = Url::parse("https://server.local/#session").unwrap();
|
||||
metadata.validate_urls().unwrap_err();
|
||||
metadata.insecure_validate_urls().unwrap_err();
|
||||
|
||||
// Insecure URL.
|
||||
let mut metadata = original_metadata;
|
||||
metadata.issuer = Url::parse("http://server.local/").unwrap();
|
||||
metadata.validate_urls().unwrap_err();
|
||||
metadata.insecure_validate_urls().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_insecure_urls() {
|
||||
let original_metadata = authorization_server_metadata();
|
||||
|
||||
let mut metadata = original_metadata.clone();
|
||||
metadata.authorization_endpoint = Url::parse("http://server.local/authorize").unwrap();
|
||||
metadata.validate_urls().unwrap_err();
|
||||
metadata.insecure_validate_urls().unwrap();
|
||||
|
||||
let mut metadata = original_metadata.clone();
|
||||
metadata.token_endpoint = Url::parse("http://server.local/token").unwrap();
|
||||
metadata.validate_urls().unwrap_err();
|
||||
metadata.insecure_validate_urls().unwrap();
|
||||
|
||||
let mut metadata = original_metadata.clone();
|
||||
metadata.registration_endpoint = Some(Url::parse("http://server.local/register").unwrap());
|
||||
metadata.validate_urls().unwrap_err();
|
||||
metadata.insecure_validate_urls().unwrap();
|
||||
|
||||
let mut metadata = original_metadata.clone();
|
||||
metadata.revocation_endpoint = Url::parse("http://server.local/revoke").unwrap();
|
||||
metadata.validate_urls().unwrap_err();
|
||||
metadata.insecure_validate_urls().unwrap();
|
||||
|
||||
let mut metadata = original_metadata.clone();
|
||||
metadata.account_management_uri = Some(Url::parse("http://server.local/account").unwrap());
|
||||
metadata.validate_urls().unwrap_err();
|
||||
metadata.insecure_validate_urls().unwrap();
|
||||
|
||||
let mut metadata = original_metadata.clone();
|
||||
metadata.device_authorization_endpoint =
|
||||
Some(Url::parse("http://server.local/device").unwrap());
|
||||
metadata.validate_urls().unwrap_err();
|
||||
metadata.insecure_validate_urls().unwrap();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,403 @@
|
|||
use std::collections::BTreeSet;
|
||||
|
||||
use serde::{de, Deserialize};
|
||||
use url::Url;
|
||||
|
||||
use super::msc2965::{
|
||||
AccountManagementAction, AuthorizationServerMetadata, CodeChallengeMethod, GrantType, Prompt,
|
||||
ResponseMode, ResponseType,
|
||||
};
|
||||
|
||||
impl<'de> Deserialize<'de> for AuthorizationServerMetadata {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let helper = AuthorizationServerMetadataDeHelper::deserialize(deserializer)?;
|
||||
|
||||
let AuthorizationServerMetadataDeHelper {
|
||||
issuer,
|
||||
authorization_endpoint,
|
||||
token_endpoint,
|
||||
registration_endpoint,
|
||||
response_types_supported,
|
||||
response_modes_supported,
|
||||
grant_types_supported,
|
||||
revocation_endpoint,
|
||||
code_challenge_methods_supported,
|
||||
account_management_uri,
|
||||
account_management_actions_supported,
|
||||
device_authorization_endpoint,
|
||||
prompt_values_supported,
|
||||
} = helper;
|
||||
|
||||
// Require `code` in `response_types_supported`.
|
||||
if !response_types_supported.contains(&ResponseType::Code) {
|
||||
return Err(de::Error::custom("missing value `code` in `response_types_supported`"));
|
||||
}
|
||||
|
||||
// Require `query` and `fragment` in `response_modes_supported`.
|
||||
if let Some(response_modes) = &response_modes_supported {
|
||||
let query_found = response_modes.contains(&ResponseMode::Query);
|
||||
let fragment_found = response_modes.contains(&ResponseMode::Fragment);
|
||||
|
||||
if !query_found && !fragment_found {
|
||||
return Err(de::Error::custom(
|
||||
"missing values `query` and `fragment` in `response_modes_supported`",
|
||||
));
|
||||
}
|
||||
if !query_found {
|
||||
return Err(de::Error::custom(
|
||||
"missing value `query` in `response_modes_supported`",
|
||||
));
|
||||
}
|
||||
if !fragment_found {
|
||||
return Err(de::Error::custom(
|
||||
"missing value `fragment` in `response_modes_supported`",
|
||||
));
|
||||
}
|
||||
}
|
||||
// If the field is missing, the default value is `["query", "fragment"]`, according to
|
||||
// RFC8414.
|
||||
let response_modes_supported = response_modes_supported
|
||||
.unwrap_or_else(|| [ResponseMode::Query, ResponseMode::Fragment].into());
|
||||
|
||||
// Require `authorization_code` and `refresh_token` in `grant_types_supported`.
|
||||
let authorization_code_found =
|
||||
grant_types_supported.contains(&GrantType::AuthorizationCode);
|
||||
let refresh_token_found = grant_types_supported.contains(&GrantType::RefreshToken);
|
||||
if !authorization_code_found && !refresh_token_found {
|
||||
return Err(de::Error::custom(
|
||||
"missing values `authorization_code` and `refresh_token` in `grant_types_supported`",
|
||||
));
|
||||
}
|
||||
if !authorization_code_found {
|
||||
return Err(de::Error::custom(
|
||||
"missing value `authorization_code` in `grant_types_supported`",
|
||||
));
|
||||
}
|
||||
if !refresh_token_found {
|
||||
return Err(de::Error::custom(
|
||||
"missing value `refresh_token` in `grant_types_supported`",
|
||||
));
|
||||
}
|
||||
|
||||
// Require `S256` in `code_challenge_methods_supported`.
|
||||
if !code_challenge_methods_supported.contains(&CodeChallengeMethod::S256) {
|
||||
return Err(de::Error::custom(
|
||||
"missing value `s256` in `code_challenge_methods_supported`",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(AuthorizationServerMetadata {
|
||||
issuer,
|
||||
authorization_endpoint,
|
||||
token_endpoint,
|
||||
registration_endpoint,
|
||||
response_types_supported,
|
||||
response_modes_supported,
|
||||
grant_types_supported,
|
||||
revocation_endpoint,
|
||||
code_challenge_methods_supported,
|
||||
account_management_uri,
|
||||
account_management_actions_supported,
|
||||
device_authorization_endpoint,
|
||||
prompt_values_supported,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthorizationServerMetadataDeHelper {
|
||||
issuer: Url,
|
||||
authorization_endpoint: Url,
|
||||
token_endpoint: Url,
|
||||
registration_endpoint: Option<Url>,
|
||||
response_types_supported: BTreeSet<ResponseType>,
|
||||
response_modes_supported: Option<BTreeSet<ResponseMode>>,
|
||||
grant_types_supported: BTreeSet<GrantType>,
|
||||
revocation_endpoint: Url,
|
||||
code_challenge_methods_supported: BTreeSet<CodeChallengeMethod>,
|
||||
account_management_uri: Option<Url>,
|
||||
#[serde(default)]
|
||||
account_management_actions_supported: BTreeSet<AccountManagementAction>,
|
||||
device_authorization_endpoint: Option<Url>,
|
||||
#[serde(default)]
|
||||
prompt_values_supported: Vec<Prompt>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use as_variant::as_variant;
|
||||
use serde_json::{from_value as from_json_value, value::Map as JsonMap, Value as JsonValue};
|
||||
use url::Url;
|
||||
|
||||
use crate::discovery::get_authorization_server_metadata::{
|
||||
msc2965::{
|
||||
AccountManagementAction, AuthorizationServerMetadata, CodeChallengeMethod, GrantType,
|
||||
ResponseMode, ResponseType,
|
||||
},
|
||||
tests::authorization_server_metadata_json,
|
||||
};
|
||||
|
||||
/// A valid `AuthorizationServerMetadata` with all fields and values, as a JSON object.
|
||||
fn authorization_server_metadata_object() -> JsonMap<String, JsonValue> {
|
||||
as_variant!(authorization_server_metadata_json(), JsonValue::Object).unwrap()
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the array value with the given key in the given object.
|
||||
///
|
||||
/// Panics if the property doesn't exist or is not an array.
|
||||
fn get_mut_array<'a>(
|
||||
object: &'a mut JsonMap<String, JsonValue>,
|
||||
key: &str,
|
||||
) -> &'a mut Vec<JsonValue> {
|
||||
object.get_mut(key).unwrap().as_array_mut().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_all_fields() {
|
||||
let metadata_object = authorization_server_metadata_object();
|
||||
let metadata =
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap();
|
||||
|
||||
assert_eq!(metadata.issuer.as_str(), "https://server.local/");
|
||||
assert_eq!(metadata.authorization_endpoint.as_str(), "https://server.local/authorize");
|
||||
assert_eq!(metadata.token_endpoint.as_str(), "https://server.local/token");
|
||||
assert_eq!(
|
||||
metadata.registration_endpoint.as_ref().map(Url::as_str),
|
||||
Some("https://server.local/register")
|
||||
);
|
||||
|
||||
assert_eq!(metadata.response_types_supported.len(), 1);
|
||||
assert!(metadata.response_types_supported.contains(&ResponseType::Code));
|
||||
|
||||
assert_eq!(metadata.response_modes_supported.len(), 2);
|
||||
assert!(metadata.response_modes_supported.contains(&ResponseMode::Query));
|
||||
assert!(metadata.response_modes_supported.contains(&ResponseMode::Fragment));
|
||||
|
||||
assert_eq!(metadata.grant_types_supported.len(), 2);
|
||||
assert!(metadata.grant_types_supported.contains(&GrantType::AuthorizationCode));
|
||||
assert!(metadata.grant_types_supported.contains(&GrantType::RefreshToken));
|
||||
|
||||
assert_eq!(metadata.revocation_endpoint.as_str(), "https://server.local/revoke");
|
||||
|
||||
assert_eq!(metadata.code_challenge_methods_supported.len(), 1);
|
||||
assert!(metadata.code_challenge_methods_supported.contains(&CodeChallengeMethod::S256));
|
||||
|
||||
assert_eq!(
|
||||
metadata.account_management_uri.as_ref().map(Url::as_str),
|
||||
Some("https://server.local/account")
|
||||
);
|
||||
assert_eq!(metadata.account_management_actions_supported.len(), 6);
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::Profile));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::SessionsList));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::SessionView));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::SessionEnd));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::AccountDeactivate));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::CrossSigningReset));
|
||||
|
||||
assert_eq!(
|
||||
metadata.device_authorization_endpoint.as_ref().map(Url::as_str),
|
||||
Some("https://server.local/device")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_no_optional_fields() {
|
||||
let mut metadata_object = authorization_server_metadata_object();
|
||||
assert!(metadata_object.remove("registration_endpoint").is_some());
|
||||
assert!(metadata_object.remove("response_modes_supported").is_some());
|
||||
assert!(metadata_object.remove("account_management_uri").is_some());
|
||||
assert!(metadata_object.remove("account_management_actions_supported").is_some());
|
||||
assert!(metadata_object.remove("device_authorization_endpoint").is_some());
|
||||
|
||||
let metadata =
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap();
|
||||
|
||||
assert_eq!(metadata.issuer.as_str(), "https://server.local/");
|
||||
assert_eq!(metadata.authorization_endpoint.as_str(), "https://server.local/authorize");
|
||||
assert_eq!(metadata.token_endpoint.as_str(), "https://server.local/token");
|
||||
assert_eq!(metadata.registration_endpoint, None);
|
||||
|
||||
assert_eq!(metadata.response_types_supported.len(), 1);
|
||||
assert!(metadata.response_types_supported.contains(&ResponseType::Code));
|
||||
|
||||
assert_eq!(metadata.response_modes_supported.len(), 2);
|
||||
assert!(metadata.response_modes_supported.contains(&ResponseMode::Query));
|
||||
assert!(metadata.response_modes_supported.contains(&ResponseMode::Fragment));
|
||||
|
||||
assert_eq!(metadata.grant_types_supported.len(), 2);
|
||||
assert!(metadata.grant_types_supported.contains(&GrantType::AuthorizationCode));
|
||||
assert!(metadata.grant_types_supported.contains(&GrantType::RefreshToken));
|
||||
|
||||
assert_eq!(metadata.revocation_endpoint.as_str(), "https://server.local/revoke");
|
||||
|
||||
assert_eq!(metadata.code_challenge_methods_supported.len(), 1);
|
||||
assert!(metadata.code_challenge_methods_supported.contains(&CodeChallengeMethod::S256));
|
||||
|
||||
assert_eq!(metadata.account_management_uri, None);
|
||||
assert_eq!(metadata.account_management_actions_supported.len(), 0);
|
||||
|
||||
assert_eq!(metadata.device_authorization_endpoint, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_additional_values() {
|
||||
let mut metadata_object = authorization_server_metadata_object();
|
||||
get_mut_array(&mut metadata_object, "response_types_supported").push("custom".into());
|
||||
get_mut_array(&mut metadata_object, "response_modes_supported").push("custom".into());
|
||||
get_mut_array(&mut metadata_object, "grant_types_supported").push("custom".into());
|
||||
get_mut_array(&mut metadata_object, "code_challenge_methods_supported")
|
||||
.push("custom".into());
|
||||
get_mut_array(&mut metadata_object, "account_management_actions_supported")
|
||||
.push("custom".into());
|
||||
|
||||
let metadata =
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap();
|
||||
|
||||
assert_eq!(metadata.issuer.as_str(), "https://server.local/");
|
||||
assert_eq!(metadata.authorization_endpoint.as_str(), "https://server.local/authorize");
|
||||
assert_eq!(metadata.token_endpoint.as_str(), "https://server.local/token");
|
||||
assert_eq!(
|
||||
metadata.registration_endpoint.as_ref().map(Url::as_str),
|
||||
Some("https://server.local/register")
|
||||
);
|
||||
|
||||
assert_eq!(metadata.response_types_supported.len(), 2);
|
||||
assert!(metadata.response_types_supported.contains(&ResponseType::Code));
|
||||
assert!(metadata.response_types_supported.contains(&ResponseType::from("custom")));
|
||||
|
||||
assert_eq!(metadata.response_modes_supported.len(), 3);
|
||||
assert!(metadata.response_modes_supported.contains(&ResponseMode::Query));
|
||||
assert!(metadata.response_modes_supported.contains(&ResponseMode::Fragment));
|
||||
assert!(metadata.response_modes_supported.contains(&ResponseMode::from("custom")));
|
||||
|
||||
assert_eq!(metadata.grant_types_supported.len(), 3);
|
||||
assert!(metadata.grant_types_supported.contains(&GrantType::AuthorizationCode));
|
||||
assert!(metadata.grant_types_supported.contains(&GrantType::RefreshToken));
|
||||
assert!(metadata.grant_types_supported.contains(&GrantType::from("custom")));
|
||||
|
||||
assert_eq!(metadata.revocation_endpoint.as_str(), "https://server.local/revoke");
|
||||
|
||||
assert_eq!(metadata.code_challenge_methods_supported.len(), 2);
|
||||
assert!(metadata.code_challenge_methods_supported.contains(&CodeChallengeMethod::S256));
|
||||
assert!(metadata
|
||||
.code_challenge_methods_supported
|
||||
.contains(&CodeChallengeMethod::from("custom")));
|
||||
|
||||
assert_eq!(
|
||||
metadata.account_management_uri.as_ref().map(Url::as_str),
|
||||
Some("https://server.local/account")
|
||||
);
|
||||
assert_eq!(metadata.account_management_actions_supported.len(), 7);
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::Profile));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::SessionsList));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::SessionView));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::SessionEnd));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::AccountDeactivate));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::CrossSigningReset));
|
||||
assert!(metadata
|
||||
.account_management_actions_supported
|
||||
.contains(&AccountManagementAction::from("custom")));
|
||||
|
||||
assert_eq!(
|
||||
metadata.device_authorization_endpoint.as_ref().map(Url::as_str),
|
||||
Some("https://server.local/device")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_missing_required_fields() {
|
||||
let original_metadata_object = authorization_server_metadata_object();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
assert!(metadata_object.remove("issuer").is_some());
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
assert!(metadata_object.remove("authorization_endpoint").is_some());
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
assert!(metadata_object.remove("token_endpoint").is_some());
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
assert!(metadata_object.remove("response_types_supported").is_some());
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
assert!(metadata_object.remove("grant_types_supported").is_some());
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
assert!(metadata_object.remove("revocation_endpoint").is_some());
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object;
|
||||
assert!(metadata_object.remove("code_challenge_methods_supported").is_some());
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_missing_required_values() {
|
||||
let original_metadata_object = authorization_server_metadata_object();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
get_mut_array(&mut metadata_object, "response_types_supported").clear();
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
get_mut_array(&mut metadata_object, "response_modes_supported").clear();
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
get_mut_array(&mut metadata_object, "response_modes_supported").remove(0);
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
get_mut_array(&mut metadata_object, "response_modes_supported").remove(1);
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
get_mut_array(&mut metadata_object, "grant_types_supported").clear();
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
get_mut_array(&mut metadata_object, "grant_types_supported").remove(0);
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object.clone();
|
||||
get_mut_array(&mut metadata_object, "grant_types_supported").remove(1);
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
|
||||
let mut metadata_object = original_metadata_object;
|
||||
get_mut_array(&mut metadata_object, "code_challenge_methods_supported").clear();
|
||||
from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue