use super::*;
fn config() -> RegistrationConfig {
let mut cfg = RegistrationConfig::new();
cfg.allowed_scopes = ScopeSet::parse("read write").unwrap();
cfg
}
fn code_grant_metadata() -> ClientMetadata {
ClientMetadata {
redirect_uris: vec!["https://app.example/cb".to_string()],
..ClientMetadata::default()
}
}
fn error_code(failure: RegistrationFailure) -> RegistrationErrorCode {
match failure {
RegistrationFailure::Invalid(e) => e.error,
other => panic!("expected an RFC 7591 s3.2.2 error, got {other:?}"),
}
}
#[test]
fn a_redirect_uri_must_be_absolute_and_carry_no_fragment() {
assert!(redirect_uri_is_registerable("https://app.example/cb"));
assert!(redirect_uri_is_registerable("https://app.example/cb?x=1"));
assert!(redirect_uri_is_registerable("com.example.app:/oauth"));
assert!(
!redirect_uri_is_registerable("/cb"),
"a relative reference is not an absolute URI (RFC 6749 s3.1.2)"
);
assert!(
!redirect_uri_is_registerable("https://app.example/cb#frag"),
"RFC 6749 s3.1.2 forbids a fragment component"
);
assert!(
!redirect_uri_is_registerable(""),
"an empty string names nowhere"
);
assert!(
!redirect_uri_is_registerable("1https://app.example/cb"),
"a scheme must start with an ALPHA (RFC 3986 s3.1)"
);
}
#[test]
fn omitted_grant_and_response_types_take_the_rfc_defaults() {
let registered = validate(&code_grant_metadata(), &config()).expect("valid");
assert_eq!(registered.grant_types, vec![GrantType::AuthorizationCode]);
assert_eq!(registered.response_types, vec!["code".to_string()]);
}
#[test]
fn grant_types_and_response_types_must_correspond() {
let mut m = code_grant_metadata();
m.grant_types = Some(vec!["authorization_code".to_string()]);
m.response_types = Some(vec![]);
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata,
"authorization_code without the code response type"
);
let mut m = code_grant_metadata();
m.grant_types = Some(vec!["refresh_token".to_string()]);
m.response_types = Some(vec!["code".to_string()]);
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata,
"the code response type without the authorization_code grant"
);
}
#[test]
fn the_implicit_response_type_is_refused() {
let mut m = code_grant_metadata();
m.grant_types = Some(vec!["implicit".to_string()]);
m.response_types = Some(vec!["token".to_string()]);
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata
);
}
#[test]
fn the_authorization_code_grant_requires_a_redirect_uri() {
let m = ClientMetadata::default();
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidRedirectUri
);
}
#[test]
fn a_malformed_redirect_uri_is_its_own_error_code() {
let mut m = code_grant_metadata();
m.redirect_uris = vec!["https://app.example/cb#frag".to_string()];
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidRedirectUri
);
}
#[test]
fn a_grant_the_deployment_does_not_offer_registrants_is_refused() {
let mut m = code_grant_metadata();
m.grant_types = Some(vec!["client_credentials".to_string()]);
m.response_types = Some(vec![]);
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata
);
let mut cfg = config();
cfg.allowed_grant_types = vec![GrantType::ClientCredentials];
let registered = validate(&m, &cfg).expect("permitted once the host says so");
assert_eq!(registered.grant_types, vec![GrantType::ClientCredentials]);
}
#[test]
fn an_unknown_grant_type_is_refused_rather_than_ignored() {
let mut m = code_grant_metadata();
m.grant_types = Some(vec!["urn:example:invented".to_string()]);
m.response_types = Some(vec![]);
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata
);
}
#[test]
fn every_registerable_auth_method_is_one_the_metadata_advertises() {
let registered = validate(&code_grant_metadata(), &config()).expect("valid");
assert_eq!(registered.token_endpoint_auth_method, "client_secret_basic");
let advertised = crate::metadata::AuthorizationServerMetadata::from_config(&ServerConfig::new(
"https://as.example",
"https://as.example/device",
))
.token_endpoint_auth_methods_supported;
for method in ["none", "client_secret_basic", "client_secret_post"] {
let mut m = code_grant_metadata();
m.token_endpoint_auth_method = Some(method.to_string());
let accepted = validate(&m, &config()).expect("this server registers this method");
assert_eq!(accepted.token_endpoint_auth_method, method);
assert!(
advertised.iter().any(|m| m == method),
"registration accepts {method} but RFC 8414 \
token_endpoint_auth_methods_supported does not advertise it, so a client registered \
this way can never authenticate at the token endpoint"
);
}
let mut m = code_grant_metadata();
m.token_endpoint_auth_method = Some("urn:example:invented".to_string());
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata
);
}
#[test]
fn a_method_the_metadata_advertises_may_still_be_unregisterable() {
let mut m = code_grant_metadata();
m.token_endpoint_auth_method = Some("private_key_jwt".to_string());
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata
);
#[cfg(all(feature = "client-assertion", feature = "jwt-p256"))]
{
let advertised = crate::metadata::AuthorizationServerMetadata::from_config(
&ServerConfig::new("https://as.example", "https://as.example/device"),
)
.token_endpoint_auth_methods_supported;
assert!(
advertised
.iter()
.any(|m| m == crate::client_assertion::PRIVATE_KEY_JWT),
"this build verifies RFC 7523 assertions, so the token endpoint really does offer \
private_key_jwt even though registration cannot record the key it would need"
);
}
}
#[test]
fn a_public_client_may_not_register_the_client_credentials_grant() {
let mut cfg = config();
cfg.allowed_grant_types = vec![GrantType::ClientCredentials];
let mut m = ClientMetadata {
token_endpoint_auth_method: Some("none".to_string()),
grant_types: Some(vec!["client_credentials".to_string()]),
..ClientMetadata::default()
};
m.response_types = Some(vec![]);
assert_eq!(
error_code(validate(&m, &cfg).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata
);
}
#[test]
fn scope_is_bounded_by_what_the_deployment_offers_registrants() {
let mut m = code_grant_metadata();
m.scope = Some("read".to_string());
let registered = validate(&m, &config()).expect("inside the ceiling");
assert_eq!(registered.scope, ScopeSet::parse("read").unwrap());
m.scope = Some("read admin".to_string());
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata,
"admin is outside the deployment's registration ceiling"
);
m.scope = Some("read".to_string());
assert_eq!(
error_code(validate(&m, &RegistrationConfig::new()).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata
);
}
#[test]
fn a_malformed_scope_is_invalid_client_metadata() {
let mut m = code_grant_metadata();
m.scope = Some("read \"write\"".to_string());
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidClientMetadata
);
}
#[test]
fn a_software_statement_is_refused_rather_than_silently_dropped() {
let mut m = code_grant_metadata();
m.software_statement = Some("eyJhbGciOiJub25lIn0.e30.".to_string());
assert_eq!(
error_code(validate(&m, &config()).unwrap_err()),
RegistrationErrorCode::InvalidSoftwareStatement
);
}
#[test]
fn unmodelled_metadata_members_are_ignored() {
let json = r#"{
"redirect_uris": ["https://app.example/cb"],
"logo_uri": "https://app.example/logo.png",
"contacts": ["ops@app.example"],
"tos_uri": "https://app.example/tos"
}"#;
let m: ClientMetadata = serde_json::from_str(json).expect("unknown members are ignored");
assert!(validate(&m, &config()).is_ok());
}