mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
Add rfc8414 metadata (#2434)
This commit is contained in:
parent
8e4980b2c1
commit
b1e7cb13a5
|
@ -60,6 +60,12 @@ Kanidm will expose its OAuth2 APIs at the following URLs:
|
|||
- rfc7662 token introspection url: `https://idm.example.com/oauth2/token/introspect`
|
||||
- rfc7009 token revoke url: `https://idm.example.com/oauth2/token/revoke`
|
||||
|
||||
Oauth2 Server Metadata - you need to substitute your OAuth2 `:client_id:` in the following urls:
|
||||
|
||||
- Oauth2 issuer uri: `https://idm.example.com/oauth2/openid/:client_id:/`
|
||||
- Oauth2 rfc8414 discovery:
|
||||
`https://idm.example.com/oauth2/openid/:client_id:/.well-known/oauth-authorization-server`
|
||||
|
||||
OpenID Connect discovery - you need to substitute your OAuth2 `:client_id:` in the following urls:
|
||||
|
||||
- OpenID connect issuer uri: `https://idm.example.com/oauth2/openid/:client_id:/`
|
||||
|
|
|
@ -224,6 +224,11 @@ pub enum SubjectType {
|
|||
Public,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
pub enum PkceAlg {
|
||||
S256,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
// WE REFUSE TO SUPPORT NONE. DONT EVEN ASK. IT WON'T HAPPEN.
|
||||
|
@ -343,6 +348,52 @@ pub struct OidcDiscoveryResponse {
|
|||
pub require_request_uri_registration: bool,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
pub struct Oauth2Rfc8414MetadataResponse {
|
||||
pub issuer: Url,
|
||||
pub authorization_endpoint: Url,
|
||||
pub token_endpoint: Url,
|
||||
|
||||
pub jwks_uri: Option<Url>,
|
||||
|
||||
// rfc7591 reg endpoint.
|
||||
pub registration_endpoint: Option<Url>,
|
||||
|
||||
pub scopes_supported: Option<Vec<String>>,
|
||||
|
||||
// For Oauth2 should be Code, Token.
|
||||
pub response_types_supported: Vec<ResponseType>,
|
||||
#[serde(default = "response_modes_supported_default")]
|
||||
pub response_modes_supported: Vec<ResponseMode>,
|
||||
#[serde(default = "grant_types_supported_default")]
|
||||
pub grant_types_supported: Vec<GrantType>,
|
||||
|
||||
#[serde(default = "token_endpoint_auth_methods_supported_default")]
|
||||
pub token_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
|
||||
|
||||
pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
|
||||
|
||||
pub service_documentation: Option<Url>,
|
||||
pub ui_locales_supported: Option<Vec<String>>,
|
||||
|
||||
pub op_policy_uri: Option<Url>,
|
||||
pub op_tos_uri: Option<Url>,
|
||||
|
||||
// rfc7009
|
||||
pub revocation_endpoint: Option<Url>,
|
||||
pub revocation_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
|
||||
|
||||
// rfc7662
|
||||
pub introspection_endpoint: Option<Url>,
|
||||
pub introspection_endpoint_auth_methods_supported: Vec<TokenEndpointAuthMethod>,
|
||||
pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<IdTokenSignAlg>>,
|
||||
|
||||
// RFC7636
|
||||
pub code_challenge_methods_supported: Vec<PkceAlg>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||
pub struct ErrorResponse {
|
||||
|
|
|
@ -32,7 +32,8 @@ use kanidmd_lib::{
|
|||
idm::ldap::{LdapBoundToken, LdapResponseState, LdapServer},
|
||||
idm::oauth2::{
|
||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AuthorisationRequest,
|
||||
AuthoriseResponse, JwkKeySet, Oauth2Error, OidcDiscoveryResponse, OidcToken,
|
||||
AuthoriseResponse, JwkKeySet, Oauth2Error, Oauth2Rfc8414MetadataResponse,
|
||||
OidcDiscoveryResponse, OidcToken,
|
||||
},
|
||||
idm::server::{IdmServer, IdmServerTransaction},
|
||||
idm::serviceaccount::ListApiTokenEvent,
|
||||
|
@ -1432,6 +1433,20 @@ impl QueryServerReadV1 {
|
|||
idms_prox_read.oauth2_openid_discovery(&client_id)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(uuid = ?eventid)
|
||||
)]
|
||||
pub async fn handle_oauth2_rfc8414_metadata(
|
||||
&self,
|
||||
client_id: String,
|
||||
eventid: Uuid,
|
||||
) -> Result<Oauth2Rfc8414MetadataResponse, OperationError> {
|
||||
let idms_prox_read = self.idms.proxy_read().await;
|
||||
idms_prox_read.oauth2_rfc8414_metadata(&client_id)
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip_all,
|
||||
|
|
|
@ -519,8 +519,6 @@ pub async fn oauth2_openid_discovery_get(
|
|||
Path(client_id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
// let client_id = req.get_url_param("client_id")?;
|
||||
|
||||
let res = state
|
||||
.qe_r_ref
|
||||
.handle_oauth2_openid_discovery(client_id, kopid.eventid)
|
||||
|
@ -540,6 +538,30 @@ pub async fn oauth2_openid_discovery_get(
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn oauth2_rfc8414_metadata_get(
|
||||
State(state): State<ServerState>,
|
||||
Path(client_id): Path<String>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> impl IntoResponse {
|
||||
let res = state
|
||||
.qe_r_ref
|
||||
.handle_oauth2_rfc8414_metadata(client_id, kopid.eventid)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(dsc) => (
|
||||
StatusCode::OK,
|
||||
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
|
||||
Json(dsc),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
error!(err = ?e, "Unable to access discovery info");
|
||||
WebError::from(e).response_with_access_control_origin_header()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn oauth2_openid_userinfo_get(
|
||||
State(state): State<ServerState>,
|
||||
|
@ -758,6 +780,12 @@ pub fn route_setup(state: ServerState) -> Router<ServerState> {
|
|||
"/oauth2/openid/:client_id/public_key.jwk",
|
||||
get(oauth2_openid_publickey_get),
|
||||
)
|
||||
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OAUTH2 DISCOVERY URLS
|
||||
.route(
|
||||
"/oauth2/openid/:client_id/.well-known/oauth-authorization-server",
|
||||
get(oauth2_rfc8414_metadata_get).options(oauth2_preflight_options),
|
||||
)
|
||||
.with_state(state.clone());
|
||||
|
||||
Router::new()
|
||||
|
|
|
@ -24,7 +24,7 @@ use kanidm_proto::constants::*;
|
|||
pub use kanidm_proto::oauth2::{
|
||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||
AccessTokenResponse, AuthorisationRequest, CodeChallengeMethod, ErrorResponse, GrantTypeReq,
|
||||
OidcDiscoveryResponse, TokenRevokeRequest,
|
||||
Oauth2Rfc8414MetadataResponse, OidcDiscoveryResponse, PkceAlg, TokenRevokeRequest,
|
||||
};
|
||||
use kanidm_proto::oauth2::{
|
||||
ClaimType, DisplayValue, GrantType, IdTokenSignAlg, ResponseMode, ResponseType, SubjectType,
|
||||
|
@ -283,6 +283,8 @@ pub struct Oauth2RS {
|
|||
// For discovery we need to build and keep a number of values.
|
||||
authorization_endpoint: Url,
|
||||
token_endpoint: Url,
|
||||
revocation_endpoint: Url,
|
||||
introspection_endpoint: Url,
|
||||
userinfo_endpoint: Url,
|
||||
jwks_uri: Url,
|
||||
scopes_supported: BTreeSet<String>,
|
||||
|
@ -533,6 +535,12 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
|||
let mut token_endpoint = self.inner.origin.clone();
|
||||
token_endpoint.set_path("/oauth2/token");
|
||||
|
||||
let mut revocation_endpoint = self.inner.origin.clone();
|
||||
revocation_endpoint.set_path("/oauth2/token/revoke");
|
||||
|
||||
let mut introspection_endpoint = self.inner.origin.clone();
|
||||
introspection_endpoint.set_path("/oauth2/token/introspect");
|
||||
|
||||
let mut userinfo_endpoint = self.inner.origin.clone();
|
||||
userinfo_endpoint.set_path(&format!("/oauth2/openid/{name}/userinfo"));
|
||||
|
||||
|
@ -571,6 +579,8 @@ impl<'a> Oauth2ResourceServersWriteTransaction<'a> {
|
|||
iss,
|
||||
authorization_endpoint,
|
||||
token_endpoint,
|
||||
revocation_endpoint,
|
||||
introspection_endpoint,
|
||||
userinfo_endpoint,
|
||||
jwks_uri,
|
||||
scopes_supported,
|
||||
|
@ -1936,6 +1946,82 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn oauth2_rfc8414_metadata(
|
||||
&self,
|
||||
client_id: &str,
|
||||
) -> Result<Oauth2Rfc8414MetadataResponse, OperationError> {
|
||||
let o2rs = self.oauth2rs.inner.rs_set.get(client_id).ok_or_else(|| {
|
||||
admin_warn!(
|
||||
"Invalid OAuth2 client_id (have you configured the OAuth2 resource server?)"
|
||||
);
|
||||
OperationError::NoMatchingEntries
|
||||
})?;
|
||||
|
||||
let issuer = o2rs.iss.clone();
|
||||
let authorization_endpoint = o2rs.authorization_endpoint.clone();
|
||||
let token_endpoint = o2rs.token_endpoint.clone();
|
||||
let revocation_endpoint = Some(o2rs.revocation_endpoint.clone());
|
||||
let introspection_endpoint = Some(o2rs.introspection_endpoint.clone());
|
||||
let jwks_uri = Some(o2rs.jwks_uri.clone());
|
||||
let scopes_supported = Some(o2rs.scopes_supported.iter().cloned().collect());
|
||||
let response_types_supported = vec![ResponseType::Code];
|
||||
let response_modes_supported = vec![ResponseMode::Query];
|
||||
let grant_types_supported = vec![GrantType::AuthorisationCode];
|
||||
|
||||
let token_endpoint_auth_methods_supported = vec![
|
||||
TokenEndpointAuthMethod::ClientSecretBasic,
|
||||
TokenEndpointAuthMethod::ClientSecretPost,
|
||||
];
|
||||
|
||||
let revocation_endpoint_auth_methods_supported = vec![
|
||||
TokenEndpointAuthMethod::ClientSecretBasic,
|
||||
TokenEndpointAuthMethod::ClientSecretPost,
|
||||
];
|
||||
|
||||
let introspection_endpoint_auth_methods_supported = vec![
|
||||
TokenEndpointAuthMethod::ClientSecretBasic,
|
||||
TokenEndpointAuthMethod::ClientSecretPost,
|
||||
];
|
||||
|
||||
let service_documentation = Some(URL_SERVICE_DOCUMENTATION.clone());
|
||||
|
||||
let require_pkce = match &o2rs.type_ {
|
||||
OauthRSType::Basic { enable_pkce, .. } => *enable_pkce,
|
||||
OauthRSType::Public { .. } => true,
|
||||
};
|
||||
|
||||
let code_challenge_methods_supported = if require_pkce {
|
||||
vec![PkceAlg::S256]
|
||||
} else {
|
||||
Vec::with_capacity(0)
|
||||
};
|
||||
|
||||
Ok(Oauth2Rfc8414MetadataResponse {
|
||||
issuer,
|
||||
authorization_endpoint,
|
||||
token_endpoint,
|
||||
jwks_uri,
|
||||
registration_endpoint: None,
|
||||
scopes_supported,
|
||||
response_types_supported,
|
||||
response_modes_supported,
|
||||
grant_types_supported,
|
||||
token_endpoint_auth_methods_supported,
|
||||
token_endpoint_auth_signing_alg_values_supported: None,
|
||||
service_documentation,
|
||||
ui_locales_supported: None,
|
||||
op_policy_uri: None,
|
||||
op_tos_uri: None,
|
||||
revocation_endpoint,
|
||||
revocation_endpoint_auth_methods_supported,
|
||||
introspection_endpoint,
|
||||
introspection_endpoint_auth_methods_supported,
|
||||
introspection_endpoint_auth_signing_alg_values_supported: None,
|
||||
code_challenge_methods_supported,
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn oauth2_openid_discovery(
|
||||
&self,
|
||||
|
@ -3527,6 +3613,114 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[idm_test]
|
||||
async fn test_idm_oauth2_rfc8414_metadata(
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
) {
|
||||
let ct = Duration::from_secs(TEST_CURRENT_TIME);
|
||||
let (_secret, _uat, _ident, _) =
|
||||
setup_oauth2_resource_server_basic(idms, ct, true, false, false).await;
|
||||
|
||||
let idms_prox_read = idms.proxy_read().await;
|
||||
|
||||
// check the discovery end point works as we expect
|
||||
assert!(
|
||||
idms_prox_read
|
||||
.oauth2_rfc8414_metadata("nosuchclient")
|
||||
.unwrap_err()
|
||||
== OperationError::NoMatchingEntries
|
||||
);
|
||||
|
||||
let discovery = idms_prox_read
|
||||
.oauth2_rfc8414_metadata("test_resource_server")
|
||||
.expect("Failed to get discovery");
|
||||
|
||||
assert!(
|
||||
discovery.issuer
|
||||
== Url::parse("https://idm.example.com/oauth2/openid/test_resource_server")
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert!(
|
||||
discovery.authorization_endpoint
|
||||
== Url::parse("https://idm.example.com/ui/oauth2").unwrap()
|
||||
);
|
||||
|
||||
assert!(
|
||||
discovery.token_endpoint == Url::parse("https://idm.example.com/oauth2/token").unwrap()
|
||||
);
|
||||
|
||||
assert!(
|
||||
discovery.jwks_uri
|
||||
== Some(
|
||||
Url::parse(
|
||||
"https://idm.example.com/oauth2/openid/test_resource_server/public_key.jwk"
|
||||
)
|
||||
.unwrap()
|
||||
)
|
||||
);
|
||||
|
||||
assert!(discovery.registration_endpoint.is_none());
|
||||
|
||||
assert!(
|
||||
discovery.scopes_supported
|
||||
== Some(vec![
|
||||
"groups".to_string(),
|
||||
OAUTH2_SCOPE_OPENID.to_string(),
|
||||
"supplement".to_string(),
|
||||
])
|
||||
);
|
||||
|
||||
assert!(discovery.response_types_supported == vec![ResponseType::Code]);
|
||||
assert!(discovery.response_modes_supported == vec![ResponseMode::Query]);
|
||||
assert!(discovery.grant_types_supported == vec![GrantType::AuthorisationCode]);
|
||||
assert!(
|
||||
discovery.token_endpoint_auth_methods_supported
|
||||
== vec![
|
||||
TokenEndpointAuthMethod::ClientSecretBasic,
|
||||
TokenEndpointAuthMethod::ClientSecretPost
|
||||
]
|
||||
);
|
||||
assert!(discovery.service_documentation.is_some());
|
||||
|
||||
assert!(discovery.ui_locales_supported.is_none());
|
||||
assert!(discovery.op_policy_uri.is_none());
|
||||
assert!(discovery.op_tos_uri.is_none());
|
||||
|
||||
assert!(
|
||||
discovery.revocation_endpoint
|
||||
== Some(Url::parse("https://idm.example.com/oauth2/token/revoke").unwrap())
|
||||
);
|
||||
assert!(
|
||||
discovery.revocation_endpoint_auth_methods_supported
|
||||
== vec![
|
||||
TokenEndpointAuthMethod::ClientSecretBasic,
|
||||
TokenEndpointAuthMethod::ClientSecretPost
|
||||
]
|
||||
);
|
||||
|
||||
assert!(
|
||||
discovery.introspection_endpoint
|
||||
== Some(Url::parse("https://idm.example.com/oauth2/token/introspect").unwrap())
|
||||
);
|
||||
assert!(
|
||||
discovery.introspection_endpoint_auth_methods_supported
|
||||
== vec![
|
||||
TokenEndpointAuthMethod::ClientSecretBasic,
|
||||
TokenEndpointAuthMethod::ClientSecretPost
|
||||
]
|
||||
);
|
||||
assert!(discovery
|
||||
.introspection_endpoint_auth_signing_alg_values_supported
|
||||
.is_none());
|
||||
|
||||
assert_eq!(
|
||||
discovery.code_challenge_methods_supported,
|
||||
vec![PkceAlg::S256]
|
||||
)
|
||||
}
|
||||
|
||||
#[idm_test]
|
||||
async fn test_idm_oauth2_openid_discovery(
|
||||
idms: &IdmServer,
|
||||
|
@ -3611,7 +3805,6 @@ mod tests {
|
|||
.unwrap()
|
||||
);
|
||||
|
||||
eprintln!("{:?}", discovery.scopes_supported);
|
||||
assert!(
|
||||
discovery.scopes_supported
|
||||
== Some(vec![
|
||||
|
|
Loading…
Reference in a new issue