Add rfc8414 metadata (#2434)

This commit is contained in:
Firstyear 2024-01-19 14:14:52 +10:00 committed by GitHub
parent 8e4980b2c1
commit b1e7cb13a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 298 additions and 5 deletions

View file

@ -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:/`

View file

@ -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 {

View file

@ -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,

View file

@ -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()

View file

@ -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![