2022-04-29 05:03:21 +02:00
|
|
|
#![deny(warnings)]
|
2024-02-21 01:52:10 +01:00
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
2022-10-01 08:08:51 +02:00
|
|
|
use std::convert::TryFrom;
|
|
|
|
use std::str::FromStr;
|
2021-06-29 06:23:39 +02:00
|
|
|
|
2023-11-24 03:53:22 +01:00
|
|
|
use compact_jwt::{JwkKeySet, JwsEs256Verifier, JwsVerifier, OidcToken, OidcUnverified};
|
2023-10-27 08:03:58 +02:00
|
|
|
use kanidm_proto::constants::uri::{OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT};
|
2023-08-21 09:16:43 +02:00
|
|
|
use kanidm_proto::constants::*;
|
2024-01-16 01:44:12 +01:00
|
|
|
use kanidm_proto::internal::Oauth2ClaimMapJoin;
|
2021-10-26 05:00:02 +02:00
|
|
|
use kanidm_proto::oauth2::{
|
|
|
|
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
2024-08-26 01:30:20 +02:00
|
|
|
AccessTokenResponse, AccessTokenType, AuthorisationResponse, GrantTypeReq,
|
|
|
|
OidcDiscoveryResponse,
|
2021-10-26 05:00:02 +02:00
|
|
|
};
|
2023-12-18 00:10:13 +01:00
|
|
|
use kanidmd_lib::prelude::{Attribute, IDM_ALL_ACCOUNTS};
|
2021-06-29 06:23:39 +02:00
|
|
|
use oauth2_ext::PkceCodeChallenge;
|
2023-08-14 00:51:44 +02:00
|
|
|
use reqwest::header::{HeaderValue, CONTENT_TYPE};
|
2023-07-05 14:26:39 +02:00
|
|
|
use reqwest::StatusCode;
|
2024-10-26 04:08:48 +02:00
|
|
|
use uri::{OAUTH2_TOKEN_ENDPOINT, OAUTH2_TOKEN_INTROSPECT_ENDPOINT, OAUTH2_TOKEN_REVOKE_ENDPOINT};
|
2025-01-08 06:41:01 +01:00
|
|
|
use url::{form_urlencoded::parse as query_parse, Url};
|
2021-06-29 06:23:39 +02:00
|
|
|
|
2022-10-24 01:50:31 +02:00
|
|
|
use kanidm_client::KanidmClient;
|
2024-10-26 04:08:48 +02:00
|
|
|
use kanidmd_testkit::{
|
|
|
|
assert_no_cache, ADMIN_TEST_PASSWORD, ADMIN_TEST_USER, NOT_ADMIN_TEST_EMAIL,
|
|
|
|
NOT_ADMIN_TEST_PASSWORD, NOT_ADMIN_TEST_USERNAME, TEST_INTEGRATION_RS_DISPLAY,
|
|
|
|
TEST_INTEGRATION_RS_GROUP_ALL, TEST_INTEGRATION_RS_ID, TEST_INTEGRATION_RS_REDIRECT_URL,
|
|
|
|
TEST_INTEGRATION_RS_URL,
|
|
|
|
};
|
2023-07-05 14:26:39 +02:00
|
|
|
|
2025-01-08 06:41:01 +01:00
|
|
|
/// Tests an OAuth 2.0 / OpenID confidential client Authorisation Client flow.
|
|
|
|
///
|
|
|
|
/// ## Arguments
|
|
|
|
///
|
|
|
|
/// * `response_mode`: If `Some`, the `response_mode` parameter to pass in the
|
|
|
|
/// `/oauth2/authorise` request.
|
|
|
|
///
|
|
|
|
/// * `response_in_fragment`: If `false`, use the `code` passed in the
|
|
|
|
/// callback URI's query parameter, and require the fragment to be empty.
|
|
|
|
///
|
|
|
|
/// If `true`, use the `code` passed in the callback URI's fragment, and
|
|
|
|
/// require the query parameter to be empty.
|
|
|
|
async fn test_oauth2_openid_basic_flow_impl(
|
|
|
|
rsclient: KanidmClient,
|
|
|
|
response_mode: Option<&str>,
|
|
|
|
response_in_fragment: bool,
|
|
|
|
) {
|
2022-04-29 05:03:21 +02:00
|
|
|
let res = rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
|
2022-04-29 05:03:21 +02:00
|
|
|
.await;
|
|
|
|
assert!(res.is_ok());
|
|
|
|
|
|
|
|
// Create an oauth2 application integration.
|
|
|
|
rsclient
|
|
|
|
.idm_oauth2_rs_basic_create(
|
2023-07-05 14:26:39 +02:00
|
|
|
TEST_INTEGRATION_RS_ID,
|
|
|
|
TEST_INTEGRATION_RS_DISPLAY,
|
|
|
|
TEST_INTEGRATION_RS_URL,
|
2022-04-29 05:03:21 +02:00
|
|
|
)
|
|
|
|
.await
|
|
|
|
.expect("Failed to create oauth2 config");
|
|
|
|
|
2024-07-31 16:02:11 +02:00
|
|
|
rsclient
|
|
|
|
.idm_oauth2_client_add_origin(
|
|
|
|
TEST_INTEGRATION_RS_ID,
|
2024-10-26 04:08:48 +02:00
|
|
|
&Url::parse(TEST_INTEGRATION_RS_REDIRECT_URL).expect("Invalid URL"),
|
2024-07-31 16:02:11 +02:00
|
|
|
)
|
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 config");
|
|
|
|
|
2022-04-29 05:03:21 +02:00
|
|
|
// Extend the admin account with extended details for openid claims.
|
|
|
|
rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.idm_person_account_create(NOT_ADMIN_TEST_USERNAME, NOT_ADMIN_TEST_USERNAME)
|
2022-09-02 06:21:20 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to create account details");
|
|
|
|
|
|
|
|
rsclient
|
2023-09-16 04:11:06 +02:00
|
|
|
.idm_person_account_set_attr(
|
2024-10-26 04:08:48 +02:00
|
|
|
NOT_ADMIN_TEST_USERNAME,
|
2023-09-16 04:11:06 +02:00
|
|
|
Attribute::Mail.as_ref(),
|
2024-10-26 04:08:48 +02:00
|
|
|
&[NOT_ADMIN_TEST_EMAIL],
|
2023-09-16 04:11:06 +02:00
|
|
|
)
|
2022-04-29 05:03:21 +02:00
|
|
|
.await
|
2022-09-02 06:21:20 +02:00
|
|
|
.expect("Failed to create account mail");
|
|
|
|
|
|
|
|
rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.idm_person_account_primary_credential_set_password(
|
|
|
|
NOT_ADMIN_TEST_USERNAME,
|
|
|
|
NOT_ADMIN_TEST_PASSWORD,
|
|
|
|
)
|
2022-09-02 06:21:20 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to configure account password");
|
2022-04-29 05:03:21 +02:00
|
|
|
|
|
|
|
rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true, true, true)
|
2022-10-09 09:11:55 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 config");
|
|
|
|
|
|
|
|
rsclient
|
|
|
|
.idm_oauth2_rs_update_scope_map(
|
2024-10-26 04:08:48 +02:00
|
|
|
TEST_INTEGRATION_RS_ID,
|
2023-09-16 04:11:06 +02:00
|
|
|
IDM_ALL_ACCOUNTS.name,
|
2023-08-21 09:16:43 +02:00
|
|
|
vec![OAUTH2_SCOPE_READ, OAUTH2_SCOPE_EMAIL, OAUTH2_SCOPE_OPENID],
|
2022-04-29 05:03:21 +02:00
|
|
|
)
|
|
|
|
.await
|
2022-10-09 09:11:55 +02:00
|
|
|
.expect("Failed to update oauth2 scopes");
|
2022-04-29 05:03:21 +02:00
|
|
|
|
2022-10-09 09:11:55 +02:00
|
|
|
rsclient
|
2023-09-16 04:11:06 +02:00
|
|
|
.idm_oauth2_rs_update_sup_scope_map(
|
2024-10-26 04:08:48 +02:00
|
|
|
TEST_INTEGRATION_RS_ID,
|
2023-09-16 04:11:06 +02:00
|
|
|
IDM_ALL_ACCOUNTS.name,
|
2024-10-26 04:08:48 +02:00
|
|
|
vec![ADMIN_TEST_USER],
|
2023-09-16 04:11:06 +02:00
|
|
|
)
|
2022-10-09 09:11:55 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 scopes");
|
|
|
|
|
|
|
|
let client_secret = rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.idm_oauth2_rs_get_basic_secret(TEST_INTEGRATION_RS_ID)
|
2022-04-29 05:03:21 +02:00
|
|
|
.await
|
|
|
|
.ok()
|
|
|
|
.flatten()
|
2022-10-09 09:11:55 +02:00
|
|
|
.expect("Failed to retrieve test_integration basic secret");
|
2022-04-29 05:03:21 +02:00
|
|
|
|
|
|
|
// Get our admin's auth token for our new client.
|
|
|
|
// We have to re-auth to update the mail field.
|
|
|
|
let res = rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.auth_simple_password(NOT_ADMIN_TEST_USERNAME, NOT_ADMIN_TEST_PASSWORD)
|
2022-04-29 05:03:21 +02:00
|
|
|
.await;
|
|
|
|
assert!(res.is_ok());
|
2022-09-02 06:21:20 +02:00
|
|
|
let oauth_test_uat = rsclient
|
2022-04-29 05:03:21 +02:00
|
|
|
.get_token()
|
|
|
|
.await
|
|
|
|
.expect("No user auth token found");
|
|
|
|
|
|
|
|
// We need a new reqwest client here.
|
|
|
|
|
|
|
|
// from here, we can now begin what would be a "interaction" to the oauth server.
|
|
|
|
// Create a new reqwest client - we'll be using this manually.
|
|
|
|
let client = reqwest::Client::builder()
|
|
|
|
.redirect(reqwest::redirect::Policy::none())
|
|
|
|
.no_proxy()
|
|
|
|
.build()
|
|
|
|
.expect("Failed to create client.");
|
|
|
|
|
|
|
|
// Step 0 - get the openid discovery details and the public key.
|
2023-07-09 04:06:40 +02:00
|
|
|
let response = client
|
|
|
|
.request(
|
|
|
|
reqwest::Method::OPTIONS,
|
2023-08-14 12:47:49 +02:00
|
|
|
rsclient.make_url("/oauth2/openid/test_integration/.well-known/openid-configuration"),
|
2023-07-09 04:06:40 +02:00
|
|
|
)
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send discovery preflight request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2023-10-14 04:39:14 +02:00
|
|
|
|
2023-07-09 04:06:40 +02:00
|
|
|
let cors_header: &str = response
|
|
|
|
.headers()
|
2023-10-14 04:39:14 +02:00
|
|
|
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
2023-07-09 04:06:40 +02:00
|
|
|
.expect("missing access-control-allow-origin header")
|
|
|
|
.to_str()
|
|
|
|
.expect("invalid access-control-allow-origin header");
|
|
|
|
assert!(cors_header.eq("*"));
|
|
|
|
|
2022-04-29 05:03:21 +02:00
|
|
|
let response = client
|
2023-08-14 12:47:49 +02:00
|
|
|
.get(rsclient.make_url("/oauth2/openid/test_integration/.well-known/openid-configuration"))
|
2022-04-29 05:03:21 +02:00
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2024-01-09 00:57:14 +01:00
|
|
|
|
|
|
|
// Assert CORS on the GET too.
|
|
|
|
let cors_header: &str = response
|
|
|
|
.headers()
|
|
|
|
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
|
|
|
.expect("missing access-control-allow-origin header")
|
|
|
|
.to_str()
|
|
|
|
.expect("invalid access-control-allow-origin header");
|
|
|
|
assert!(cors_header.eq("*"));
|
|
|
|
|
2022-04-29 05:03:21 +02:00
|
|
|
assert_no_cache!(response);
|
|
|
|
|
|
|
|
let discovery: OidcDiscoveryResponse = response
|
|
|
|
.json()
|
|
|
|
.await
|
|
|
|
.expect("Failed to access response body");
|
|
|
|
|
2022-07-30 14:10:24 +02:00
|
|
|
tracing::trace!(?discovery);
|
|
|
|
|
2022-04-29 05:03:21 +02:00
|
|
|
// Most values are checked in idm/oauth2.rs, but we want to sanity check
|
|
|
|
// the urls here as an extended function smoke test.
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(
|
|
|
|
discovery.issuer,
|
|
|
|
rsclient.make_url("/oauth2/openid/test_integration")
|
|
|
|
);
|
2022-04-29 05:03:21 +02:00
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(
|
|
|
|
discovery.authorization_endpoint,
|
|
|
|
rsclient.make_url("/ui/oauth2")
|
|
|
|
);
|
2022-04-29 05:03:21 +02:00
|
|
|
|
2024-10-26 04:08:48 +02:00
|
|
|
assert_eq!(
|
|
|
|
discovery.token_endpoint,
|
|
|
|
rsclient.make_url(OAUTH2_TOKEN_ENDPOINT)
|
|
|
|
);
|
2022-04-29 05:03:21 +02:00
|
|
|
|
|
|
|
assert!(
|
|
|
|
discovery.userinfo_endpoint
|
2023-08-14 12:47:49 +02:00
|
|
|
== Some(rsclient.make_url("/oauth2/openid/test_integration/userinfo"))
|
2022-04-29 05:03:21 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
assert!(
|
2023-08-14 12:47:49 +02:00
|
|
|
discovery.jwks_uri == rsclient.make_url("/oauth2/openid/test_integration/public_key.jwk")
|
2022-04-29 05:03:21 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
// Step 0 - get the jwks public key.
|
|
|
|
let response = client
|
2023-08-14 12:47:49 +02:00
|
|
|
.get(rsclient.make_url("/oauth2/openid/test_integration/public_key.jwk"))
|
2022-04-29 05:03:21 +02:00
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2022-04-29 05:03:21 +02:00
|
|
|
assert_no_cache!(response);
|
|
|
|
|
|
|
|
let mut jwk_set: JwkKeySet = response
|
|
|
|
.json()
|
|
|
|
.await
|
|
|
|
.expect("Failed to access response body");
|
|
|
|
|
|
|
|
let public_jwk = jwk_set.keys.pop().expect("No public key in set!");
|
|
|
|
|
2023-11-24 03:53:22 +01:00
|
|
|
let jws_validator = JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
|
2022-04-29 05:03:21 +02:00
|
|
|
|
|
|
|
// Step 1 - the Oauth2 Resource Server would send a redirect to the authorisation
|
|
|
|
// server, where the url contains a series of authorisation request parameters.
|
|
|
|
//
|
|
|
|
// Since we are a client, we can just "pretend" we got the redirect, and issue the
|
|
|
|
// get call directly. This should be a 200. (?)
|
|
|
|
|
|
|
|
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
|
|
|
|
|
2025-01-08 06:41:01 +01:00
|
|
|
let mut query = vec![
|
|
|
|
("response_type", "code"),
|
|
|
|
("client_id", TEST_INTEGRATION_RS_ID),
|
|
|
|
("state", "YWJjZGVm"),
|
|
|
|
("code_challenge", pkce_code_challenge.as_str()),
|
|
|
|
("code_challenge_method", "S256"),
|
|
|
|
("redirect_uri", TEST_INTEGRATION_RS_REDIRECT_URL),
|
|
|
|
("scope", "email read openid"),
|
|
|
|
("max_age", "1"),
|
|
|
|
];
|
|
|
|
|
|
|
|
if let Some(response_mode) = response_mode {
|
|
|
|
query.push(("response_mode", response_mode));
|
|
|
|
}
|
|
|
|
|
2022-04-29 05:03:21 +02:00
|
|
|
let response = client
|
2023-10-27 08:03:58 +02:00
|
|
|
.get(rsclient.make_url(OAUTH2_AUTHORISE))
|
2022-09-02 06:21:20 +02:00
|
|
|
.bearer_auth(oauth_test_uat.clone())
|
2025-01-08 06:41:01 +01:00
|
|
|
.query(&query)
|
2022-04-29 05:03:21 +02:00
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2022-04-29 05:03:21 +02:00
|
|
|
assert_no_cache!(response);
|
|
|
|
|
2022-06-20 03:37:39 +02:00
|
|
|
let consent_req: AuthorisationResponse = response
|
2022-04-29 05:03:21 +02:00
|
|
|
.json()
|
|
|
|
.await
|
|
|
|
.expect("Failed to access response body");
|
|
|
|
|
2022-10-09 09:11:55 +02:00
|
|
|
let consent_token = if let AuthorisationResponse::ConsentRequested {
|
|
|
|
consent_token,
|
|
|
|
scopes,
|
|
|
|
..
|
|
|
|
} = consent_req
|
|
|
|
{
|
|
|
|
// Note the supplemental scope here (admin)
|
2024-10-26 04:08:48 +02:00
|
|
|
dbg!(&scopes);
|
2024-08-21 02:32:56 +02:00
|
|
|
assert!(scopes.contains("admin"));
|
2022-10-09 09:11:55 +02:00
|
|
|
consent_token
|
|
|
|
} else {
|
|
|
|
unreachable!();
|
|
|
|
};
|
2022-06-20 03:37:39 +02:00
|
|
|
|
2022-04-29 05:03:21 +02:00
|
|
|
// Step 2 - we now send the consent get to the server which yields a redirect with a
|
|
|
|
// state and code.
|
|
|
|
|
|
|
|
let response = client
|
2023-10-27 08:03:58 +02:00
|
|
|
.get(rsclient.make_url(OAUTH2_AUTHORISE_PERMIT))
|
2022-09-02 06:21:20 +02:00
|
|
|
.bearer_auth(oauth_test_uat)
|
2022-06-20 03:37:39 +02:00
|
|
|
.query(&[("token", consent_token.as_str())])
|
2022-04-29 05:03:21 +02:00
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send request.");
|
|
|
|
|
|
|
|
// This should yield a 302 redirect with some query params.
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::FOUND);
|
2022-04-29 05:03:21 +02:00
|
|
|
assert_no_cache!(response);
|
|
|
|
|
|
|
|
// And we should have a URL in the location header.
|
|
|
|
let redir_str = response
|
|
|
|
.headers()
|
|
|
|
.get("Location")
|
2023-01-28 04:52:44 +01:00
|
|
|
.and_then(|hv| hv.to_str().ok().map(str::to_string))
|
2022-04-29 05:03:21 +02:00
|
|
|
.expect("Invalid redirect url");
|
|
|
|
|
|
|
|
// Now check it's content
|
|
|
|
let redir_url = Url::parse(&redir_str).expect("Url parse failure");
|
2025-01-08 06:41:01 +01:00
|
|
|
let pairs: BTreeMap<_, _> = if response_in_fragment {
|
|
|
|
assert!(redir_url.query().is_none());
|
|
|
|
let fragment = redir_url.fragment().expect("missing URL fragment");
|
|
|
|
query_parse(fragment.as_bytes()).collect()
|
|
|
|
} else {
|
|
|
|
// response_mode = query is default for response_type = code
|
|
|
|
assert!(redir_url.fragment().is_none());
|
|
|
|
redir_url.query_pairs().collect()
|
|
|
|
};
|
2022-04-29 05:03:21 +02:00
|
|
|
|
|
|
|
// We should have state and code.
|
|
|
|
let code = pairs.get("code").expect("code not found!");
|
|
|
|
let state = pairs.get("state").expect("state not found!");
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(state, "YWJjZGVm");
|
2022-04-29 05:03:21 +02:00
|
|
|
|
|
|
|
// Step 3 - the "resource server" then uses this state and code to directly contact
|
|
|
|
// the authorisation server to request a token.
|
|
|
|
|
2023-04-20 00:34:21 +02:00
|
|
|
let form_req: AccessTokenRequest = GrantTypeReq::AuthorizationCode {
|
2022-04-29 05:03:21 +02:00
|
|
|
code: code.to_string(),
|
2024-10-26 04:08:48 +02:00
|
|
|
redirect_uri: Url::parse(TEST_INTEGRATION_RS_REDIRECT_URL).expect("Invalid URL"),
|
2022-04-29 05:03:21 +02:00
|
|
|
code_verifier: Some(pkce_code_verifier.secret().clone()),
|
2023-04-20 00:34:21 +02:00
|
|
|
}
|
|
|
|
.into();
|
2022-04-29 05:03:21 +02:00
|
|
|
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_ENDPOINT))
|
|
|
|
.basic_auth(TEST_INTEGRATION_RS_ID, Some(client_secret.clone()))
|
2022-04-29 05:03:21 +02:00
|
|
|
.form(&form_req)
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send code exchange request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2024-01-09 00:57:14 +01:00
|
|
|
|
|
|
|
let cors_header: &str = response
|
|
|
|
.headers()
|
|
|
|
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
|
|
|
.expect("missing access-control-allow-origin header")
|
|
|
|
.to_str()
|
|
|
|
.expect("invalid access-control-allow-origin header");
|
|
|
|
assert!(cors_header.eq("*"));
|
|
|
|
|
2023-08-14 00:51:44 +02:00
|
|
|
assert!(
|
|
|
|
response.headers().get(CONTENT_TYPE) == Some(&HeaderValue::from_static(APPLICATION_JSON))
|
|
|
|
);
|
2022-04-29 05:03:21 +02:00
|
|
|
assert_no_cache!(response);
|
|
|
|
|
|
|
|
// The body is a json AccessTokenResponse
|
|
|
|
|
|
|
|
let atr = response
|
|
|
|
.json::<AccessTokenResponse>()
|
|
|
|
.await
|
|
|
|
.expect("Unable to decode AccessTokenResponse");
|
|
|
|
|
|
|
|
// Step 4 - inspect the granted token.
|
|
|
|
let intr_request = AccessTokenIntrospectRequest {
|
|
|
|
token: atr.access_token.clone(),
|
|
|
|
token_type_hint: None,
|
|
|
|
};
|
|
|
|
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_INTROSPECT_ENDPOINT))
|
|
|
|
.basic_auth(TEST_INTEGRATION_RS_ID, Some(client_secret.clone()))
|
2022-04-29 05:03:21 +02:00
|
|
|
.form(&intr_request)
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send token introspect request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2023-08-15 07:42:15 +02:00
|
|
|
tracing::trace!("{:?}", response.headers());
|
2023-08-14 00:51:44 +02:00
|
|
|
assert!(
|
|
|
|
response.headers().get(CONTENT_TYPE) == Some(&HeaderValue::from_static(APPLICATION_JSON))
|
|
|
|
);
|
2022-04-29 05:03:21 +02:00
|
|
|
assert_no_cache!(response);
|
|
|
|
|
|
|
|
let tir = response
|
|
|
|
.json::<AccessTokenIntrospectResponse>()
|
|
|
|
.await
|
|
|
|
.expect("Unable to decode AccessTokenIntrospectResponse");
|
|
|
|
|
|
|
|
assert!(tir.active);
|
2024-11-30 07:56:17 +01:00
|
|
|
assert!(!tir.scope.is_empty());
|
2024-10-26 04:08:48 +02:00
|
|
|
assert_eq!(tir.client_id.as_deref(), Some(TEST_INTEGRATION_RS_ID));
|
|
|
|
assert_eq!(
|
|
|
|
tir.username.as_deref(),
|
|
|
|
Some(format!("{}@localhost", NOT_ADMIN_TEST_USERNAME).as_str())
|
|
|
|
);
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(tir.token_type, Some(AccessTokenType::Bearer));
|
2022-04-29 05:03:21 +02:00
|
|
|
assert!(tir.exp.is_some());
|
|
|
|
assert!(tir.iat.is_some());
|
|
|
|
assert!(tir.nbf.is_some());
|
|
|
|
assert!(tir.sub.is_some());
|
2024-10-26 04:08:48 +02:00
|
|
|
assert_eq!(tir.aud.as_deref(), Some(TEST_INTEGRATION_RS_ID));
|
2022-04-29 05:03:21 +02:00
|
|
|
assert!(tir.iss.is_none());
|
|
|
|
assert!(tir.jti.is_none());
|
|
|
|
|
|
|
|
// Step 5 - check that the id_token (openid) matches the userinfo endpoint.
|
|
|
|
let oidc_unverified =
|
|
|
|
OidcUnverified::from_str(atr.id_token.as_ref().unwrap()).expect("Failed to parse id_token");
|
|
|
|
|
2023-11-24 03:53:22 +01:00
|
|
|
let oidc = jws_validator
|
|
|
|
.verify(&oidc_unverified)
|
|
|
|
.expect("Failed to verify oidc")
|
|
|
|
.verify_exp(0)
|
|
|
|
.expect("Failed to check exp");
|
2022-04-29 05:03:21 +02:00
|
|
|
|
|
|
|
// This is mostly checked inside of idm/oauth2.rs. This is more to check the oidc
|
|
|
|
// token and the userinfo endpoints.
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(
|
|
|
|
oidc.iss,
|
|
|
|
rsclient.make_url("/oauth2/openid/test_integration")
|
|
|
|
);
|
2022-09-02 06:21:20 +02:00
|
|
|
eprintln!("{:?}", oidc.s_claims.email);
|
2024-10-26 04:08:48 +02:00
|
|
|
assert_eq!(oidc.s_claims.email.as_deref(), Some(NOT_ADMIN_TEST_EMAIL));
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(oidc.s_claims.email_verified, Some(true));
|
2023-07-07 10:53:31 +02:00
|
|
|
|
|
|
|
let response = client
|
2023-08-14 12:47:49 +02:00
|
|
|
.get(rsclient.make_url("/oauth2/openid/test_integration/userinfo"))
|
2023-07-07 10:53:31 +02:00
|
|
|
.bearer_auth(atr.access_token.clone())
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send userinfo request.");
|
|
|
|
|
2023-08-15 07:42:15 +02:00
|
|
|
tracing::trace!("{:?}", response.headers());
|
2023-08-14 00:51:44 +02:00
|
|
|
assert!(
|
|
|
|
response.headers().get(CONTENT_TYPE) == Some(&HeaderValue::from_static(APPLICATION_JSON))
|
|
|
|
);
|
2023-07-07 10:53:31 +02:00
|
|
|
let userinfo = response
|
|
|
|
.json::<OidcToken>()
|
|
|
|
.await
|
|
|
|
.expect("Unable to decode OidcToken from userinfo");
|
|
|
|
|
|
|
|
eprintln!("userinfo {userinfo:?}");
|
|
|
|
eprintln!("oidc {oidc:?}");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(userinfo, oidc);
|
2023-07-07 10:53:31 +02:00
|
|
|
|
2025-02-04 07:22:32 +01:00
|
|
|
let response = client
|
|
|
|
.post(rsclient.make_url("/oauth2/openid/test_integration/userinfo"))
|
|
|
|
.bearer_auth(atr.access_token.clone())
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send userinfo POST request.");
|
|
|
|
|
|
|
|
tracing::trace!("{:?}", response.headers());
|
|
|
|
assert!(
|
|
|
|
response.headers().get(CONTENT_TYPE) == Some(&HeaderValue::from_static(APPLICATION_JSON))
|
|
|
|
);
|
|
|
|
let userinfo_post = response
|
|
|
|
.json::<OidcToken>()
|
|
|
|
.await
|
|
|
|
.expect("Unable to decode OidcToken from POST userinfo");
|
|
|
|
|
|
|
|
assert_eq!(userinfo_post, userinfo);
|
|
|
|
|
2024-02-01 03:00:29 +01:00
|
|
|
// Step 6 - Show that our client can perform a client credentials grant
|
|
|
|
|
|
|
|
let form_req: AccessTokenRequest = GrantTypeReq::ClientCredentials {
|
|
|
|
scope: Some(BTreeSet::from([
|
|
|
|
"email".to_string(),
|
|
|
|
"read".to_string(),
|
|
|
|
"openid".to_string(),
|
|
|
|
])),
|
|
|
|
}
|
|
|
|
.into();
|
|
|
|
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_ENDPOINT))
|
|
|
|
.basic_auth(TEST_INTEGRATION_RS_ID, Some(client_secret.clone()))
|
2024-02-01 03:00:29 +01:00
|
|
|
.form(&form_req)
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send client credentials request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2024-02-01 03:00:29 +01:00
|
|
|
|
|
|
|
let atr = response
|
|
|
|
.json::<AccessTokenResponse>()
|
|
|
|
.await
|
|
|
|
.expect("Unable to decode AccessTokenResponse");
|
|
|
|
|
|
|
|
// Step 7 - inspect the granted client credentials token.
|
|
|
|
let intr_request = AccessTokenIntrospectRequest {
|
|
|
|
token: atr.access_token.clone(),
|
|
|
|
token_type_hint: None,
|
|
|
|
};
|
|
|
|
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_INTROSPECT_ENDPOINT))
|
|
|
|
.basic_auth(TEST_INTEGRATION_RS_ID, Some(client_secret))
|
2024-02-01 03:00:29 +01:00
|
|
|
.form(&intr_request)
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send token introspect request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2024-02-01 03:00:29 +01:00
|
|
|
|
|
|
|
let tir = response
|
|
|
|
.json::<AccessTokenIntrospectResponse>()
|
|
|
|
.await
|
|
|
|
.expect("Unable to decode AccessTokenIntrospectResponse");
|
|
|
|
|
|
|
|
assert!(tir.active);
|
2024-11-30 07:56:17 +01:00
|
|
|
assert!(!tir.scope.is_empty());
|
2024-10-26 04:08:48 +02:00
|
|
|
assert_eq!(tir.client_id.as_deref(), Some(TEST_INTEGRATION_RS_ID));
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(tir.username.as_deref(), Some("test_integration@localhost"));
|
|
|
|
assert_eq!(tir.token_type, Some(AccessTokenType::Bearer));
|
2024-02-01 03:00:29 +01:00
|
|
|
|
2023-07-07 10:53:31 +02:00
|
|
|
// auth back with admin so we can test deleting things
|
|
|
|
let res = rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
|
2023-07-07 10:53:31 +02:00
|
|
|
.await;
|
|
|
|
assert!(res.is_ok());
|
|
|
|
rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.idm_oauth2_rs_delete_sup_scope_map(TEST_INTEGRATION_RS_ID, TEST_INTEGRATION_RS_GROUP_ALL)
|
2023-07-07 10:53:31 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 scopes");
|
|
|
|
}
|
|
|
|
|
2025-01-08 06:41:01 +01:00
|
|
|
/// Test an OAuth 2.0/OpenID confidential client Authorisation Code flow, with
|
|
|
|
/// `response_mode` unset.
|
|
|
|
///
|
|
|
|
/// The response should be returned as a query parameter.
|
2023-07-07 10:53:31 +02:00
|
|
|
#[kanidmd_testkit::test]
|
2025-01-08 06:41:01 +01:00
|
|
|
async fn test_oauth2_openid_basic_flow_mode_unset(rsclient: KanidmClient) {
|
|
|
|
test_oauth2_openid_basic_flow_impl(rsclient, None, false).await;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Test an OAuth 2.0/OpenID confidential client Authorisation Code flow, with
|
|
|
|
/// `response_mode=query`.
|
|
|
|
///
|
|
|
|
/// The response should be returned as a query parameter.
|
|
|
|
#[kanidmd_testkit::test]
|
|
|
|
async fn test_oauth2_openid_basic_flow_mode_query(rsclient: KanidmClient) {
|
|
|
|
test_oauth2_openid_basic_flow_impl(rsclient, Some("query"), false).await;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Test an OAuth 2.0/OpenID confidential client Authorisation Code flow, with
|
|
|
|
/// `response_mode=fragment`.
|
|
|
|
///
|
|
|
|
/// The response should be returned in the URI's fragment.
|
|
|
|
#[kanidmd_testkit::test]
|
|
|
|
async fn test_oauth2_openid_basic_flow_mode_fragment(rsclient: KanidmClient) {
|
|
|
|
test_oauth2_openid_basic_flow_impl(rsclient, Some("fragment"), true).await;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Tests an OAuth 2.0 / OpenID public client Authorisation Client flow.
|
|
|
|
///
|
|
|
|
/// ## Arguments
|
|
|
|
///
|
|
|
|
/// * `response_mode`: If `Some`, the `response_mode` parameter to pass in the
|
|
|
|
/// `/oauth2/authorise` request.
|
|
|
|
///
|
|
|
|
/// * `response_in_fragment`: If `false`, use the `code` passed in the
|
|
|
|
/// callback URI's query parameter, and require the fragment to be empty.
|
|
|
|
///
|
|
|
|
/// If `true`, use the `code` passed in the callback URI's fragment, and
|
|
|
|
/// require the query parameter to be empty.
|
|
|
|
async fn test_oauth2_openid_public_flow_impl(
|
|
|
|
rsclient: KanidmClient,
|
|
|
|
response_mode: Option<&str>,
|
|
|
|
response_in_fragment: bool,
|
|
|
|
) {
|
2023-07-07 10:53:31 +02:00
|
|
|
let res = rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
|
2023-07-07 10:53:31 +02:00
|
|
|
.await;
|
|
|
|
assert!(res.is_ok());
|
|
|
|
|
|
|
|
// Create an oauth2 application integration.
|
|
|
|
rsclient
|
|
|
|
.idm_oauth2_rs_public_create(
|
|
|
|
TEST_INTEGRATION_RS_ID,
|
|
|
|
TEST_INTEGRATION_RS_DISPLAY,
|
|
|
|
TEST_INTEGRATION_RS_URL,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
.expect("Failed to create oauth2 config");
|
|
|
|
|
2024-07-31 16:02:11 +02:00
|
|
|
rsclient
|
|
|
|
.idm_oauth2_client_add_origin(
|
|
|
|
TEST_INTEGRATION_RS_ID,
|
2024-10-26 04:08:48 +02:00
|
|
|
&Url::parse(TEST_INTEGRATION_RS_REDIRECT_URL).expect("Invalid URL"),
|
2024-07-31 16:02:11 +02:00
|
|
|
)
|
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 config");
|
|
|
|
|
2023-07-07 10:53:31 +02:00
|
|
|
// Extend the admin account with extended details for openid claims.
|
|
|
|
rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.idm_person_account_create(NOT_ADMIN_TEST_USERNAME, NOT_ADMIN_TEST_USERNAME)
|
2023-07-07 10:53:31 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to create account details");
|
|
|
|
|
|
|
|
rsclient
|
2023-09-16 04:11:06 +02:00
|
|
|
.idm_person_account_set_attr(
|
2024-10-26 04:08:48 +02:00
|
|
|
NOT_ADMIN_TEST_USERNAME,
|
2023-09-16 04:11:06 +02:00
|
|
|
Attribute::Mail.as_ref(),
|
2024-10-26 04:08:48 +02:00
|
|
|
&[NOT_ADMIN_TEST_EMAIL],
|
2023-09-16 04:11:06 +02:00
|
|
|
)
|
2023-07-07 10:53:31 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to create account mail");
|
|
|
|
|
|
|
|
rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.idm_person_account_primary_credential_set_password(
|
|
|
|
NOT_ADMIN_TEST_USERNAME,
|
|
|
|
ADMIN_TEST_PASSWORD,
|
|
|
|
)
|
2023-07-07 10:53:31 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to configure account password");
|
|
|
|
|
|
|
|
rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.idm_oauth2_rs_update(TEST_INTEGRATION_RS_ID, None, None, None, true, true, true)
|
2023-07-07 10:53:31 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 config");
|
|
|
|
|
|
|
|
rsclient
|
|
|
|
.idm_oauth2_rs_update_scope_map(
|
2024-10-26 04:08:48 +02:00
|
|
|
TEST_INTEGRATION_RS_ID,
|
2023-09-16 04:11:06 +02:00
|
|
|
IDM_ALL_ACCOUNTS.name,
|
2023-08-21 09:16:43 +02:00
|
|
|
vec![OAUTH2_SCOPE_READ, OAUTH2_SCOPE_EMAIL, OAUTH2_SCOPE_OPENID],
|
2023-07-07 10:53:31 +02:00
|
|
|
)
|
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 scopes");
|
|
|
|
|
|
|
|
rsclient
|
2023-09-16 04:11:06 +02:00
|
|
|
.idm_oauth2_rs_update_sup_scope_map(
|
2024-10-26 04:08:48 +02:00
|
|
|
TEST_INTEGRATION_RS_ID,
|
2023-09-16 04:11:06 +02:00
|
|
|
IDM_ALL_ACCOUNTS.name,
|
2024-10-26 04:08:48 +02:00
|
|
|
vec![ADMIN_TEST_USER],
|
2023-09-16 04:11:06 +02:00
|
|
|
)
|
2023-07-07 10:53:31 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 scopes");
|
|
|
|
|
2024-01-16 01:44:12 +01:00
|
|
|
// Add a custom claim map.
|
|
|
|
rsclient
|
|
|
|
.idm_oauth2_rs_update_claim_map(
|
2024-10-26 04:08:48 +02:00
|
|
|
TEST_INTEGRATION_RS_ID,
|
2024-01-16 01:44:12 +01:00
|
|
|
"test_claim",
|
|
|
|
IDM_ALL_ACCOUNTS.name,
|
|
|
|
&["claim_a".to_string(), "claim_b".to_string()],
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 claims");
|
|
|
|
|
|
|
|
// Set an alternate join
|
|
|
|
rsclient
|
|
|
|
.idm_oauth2_rs_update_claim_map_join(
|
2024-10-26 04:08:48 +02:00
|
|
|
TEST_INTEGRATION_RS_ID,
|
2024-01-16 01:44:12 +01:00
|
|
|
"test_claim",
|
|
|
|
Oauth2ClaimMapJoin::Ssv,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 claims");
|
|
|
|
|
2023-07-07 10:53:31 +02:00
|
|
|
// Get our admin's auth token for our new client.
|
|
|
|
// We have to re-auth to update the mail field.
|
|
|
|
let res = rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.auth_simple_password(NOT_ADMIN_TEST_USERNAME, ADMIN_TEST_PASSWORD)
|
2023-07-07 10:53:31 +02:00
|
|
|
.await;
|
|
|
|
assert!(res.is_ok());
|
|
|
|
let oauth_test_uat = rsclient
|
|
|
|
.get_token()
|
|
|
|
.await
|
|
|
|
.expect("No user auth token found");
|
|
|
|
|
|
|
|
// We need a new reqwest client here.
|
|
|
|
|
|
|
|
// from here, we can now begin what would be a "interaction" to the oauth server.
|
|
|
|
// Create a new reqwest client - we'll be using this manually.
|
|
|
|
let client = reqwest::Client::builder()
|
|
|
|
.redirect(reqwest::redirect::Policy::none())
|
|
|
|
.no_proxy()
|
|
|
|
.build()
|
|
|
|
.expect("Failed to create client.");
|
|
|
|
|
|
|
|
// Step 0 - get the jwks public key.
|
|
|
|
let response = client
|
2023-08-14 12:47:49 +02:00
|
|
|
.get(rsclient.make_url("/oauth2/openid/test_integration/public_key.jwk"))
|
2023-07-07 10:53:31 +02:00
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2023-07-07 10:53:31 +02:00
|
|
|
assert_no_cache!(response);
|
|
|
|
|
|
|
|
let mut jwk_set: JwkKeySet = response
|
|
|
|
.json()
|
|
|
|
.await
|
|
|
|
.expect("Failed to access response body");
|
|
|
|
|
|
|
|
let public_jwk = jwk_set.keys.pop().expect("No public key in set!");
|
|
|
|
|
2023-11-24 03:53:22 +01:00
|
|
|
let jws_validator = JwsEs256Verifier::try_from(&public_jwk).expect("failed to build validator");
|
2023-07-07 10:53:31 +02:00
|
|
|
|
|
|
|
// Step 1 - the Oauth2 Resource Server would send a redirect to the authorisation
|
|
|
|
// server, where the url contains a series of authorisation request parameters.
|
|
|
|
//
|
|
|
|
// Since we are a client, we can just "pretend" we got the redirect, and issue the
|
|
|
|
// get call directly. This should be a 200. (?)
|
|
|
|
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
|
|
|
|
|
2025-01-08 06:41:01 +01:00
|
|
|
let mut query = vec![
|
|
|
|
("response_type", "code"),
|
|
|
|
("client_id", TEST_INTEGRATION_RS_ID),
|
|
|
|
("state", "YWJjZGVm"),
|
|
|
|
("code_challenge", pkce_code_challenge.as_str()),
|
|
|
|
("code_challenge_method", "S256"),
|
|
|
|
("redirect_uri", TEST_INTEGRATION_RS_REDIRECT_URL),
|
|
|
|
("scope", "email read openid"),
|
|
|
|
];
|
|
|
|
|
|
|
|
if let Some(response_mode) = response_mode {
|
|
|
|
query.push(("response_mode", response_mode));
|
|
|
|
}
|
|
|
|
|
2023-07-07 10:53:31 +02:00
|
|
|
let response = client
|
2023-10-27 08:03:58 +02:00
|
|
|
.get(rsclient.make_url(OAUTH2_AUTHORISE))
|
2023-07-07 10:53:31 +02:00
|
|
|
.bearer_auth(oauth_test_uat.clone())
|
2025-01-08 06:41:01 +01:00
|
|
|
.query(&query)
|
2023-07-07 10:53:31 +02:00
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2023-07-07 10:53:31 +02:00
|
|
|
assert_no_cache!(response);
|
|
|
|
|
|
|
|
let consent_req: AuthorisationResponse = response
|
|
|
|
.json()
|
|
|
|
.await
|
|
|
|
.expect("Failed to access response body");
|
|
|
|
|
|
|
|
let consent_token = if let AuthorisationResponse::ConsentRequested {
|
|
|
|
consent_token,
|
|
|
|
scopes,
|
|
|
|
..
|
|
|
|
} = consent_req
|
|
|
|
{
|
|
|
|
// Note the supplemental scope here (admin)
|
2024-10-26 04:08:48 +02:00
|
|
|
assert!(scopes.contains(ADMIN_TEST_USER));
|
2023-07-07 10:53:31 +02:00
|
|
|
consent_token
|
|
|
|
} else {
|
|
|
|
unreachable!();
|
|
|
|
};
|
|
|
|
|
|
|
|
// Step 2 - we now send the consent get to the server which yields a redirect with a
|
|
|
|
// state and code.
|
|
|
|
let response = client
|
2023-10-27 08:03:58 +02:00
|
|
|
.get(rsclient.make_url(OAUTH2_AUTHORISE_PERMIT))
|
2023-07-07 10:53:31 +02:00
|
|
|
.bearer_auth(oauth_test_uat)
|
|
|
|
.query(&[("token", consent_token.as_str())])
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send request.");
|
|
|
|
|
|
|
|
// This should yield a 302 redirect with some query params.
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::FOUND);
|
2023-07-07 10:53:31 +02:00
|
|
|
assert_no_cache!(response);
|
|
|
|
|
|
|
|
// And we should have a URL in the location header.
|
|
|
|
let redir_str = response
|
|
|
|
.headers()
|
|
|
|
.get("Location")
|
|
|
|
.and_then(|hv| hv.to_str().ok().map(str::to_string))
|
|
|
|
.expect("Invalid redirect url");
|
|
|
|
|
|
|
|
// Now check it's content
|
|
|
|
let redir_url = Url::parse(&redir_str).expect("Url parse failure");
|
|
|
|
|
2025-01-08 06:41:01 +01:00
|
|
|
let pairs: BTreeMap<_, _> = if response_in_fragment {
|
|
|
|
assert!(redir_url.query().is_none());
|
|
|
|
let fragment = redir_url.fragment().expect("missing URL fragment");
|
|
|
|
query_parse(fragment.as_bytes()).collect()
|
|
|
|
} else {
|
|
|
|
// response_mode = query is default for response_type = code
|
|
|
|
assert!(redir_url.fragment().is_none());
|
|
|
|
redir_url.query_pairs().collect()
|
|
|
|
};
|
2023-07-07 10:53:31 +02:00
|
|
|
|
2025-01-08 06:41:01 +01:00
|
|
|
// We should have state and code.
|
2023-07-07 10:53:31 +02:00
|
|
|
let code = pairs.get("code").expect("code not found!");
|
|
|
|
let state = pairs.get("state").expect("state not found!");
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(state, "YWJjZGVm");
|
2023-07-07 10:53:31 +02:00
|
|
|
|
|
|
|
// Step 3 - the "resource server" then uses this state and code to directly contact
|
|
|
|
// the authorisation server to request a token.
|
|
|
|
|
|
|
|
let form_req = AccessTokenRequest {
|
|
|
|
grant_type: GrantTypeReq::AuthorizationCode {
|
|
|
|
code: code.to_string(),
|
2024-10-26 04:08:48 +02:00
|
|
|
redirect_uri: Url::parse(TEST_INTEGRATION_RS_REDIRECT_URL).expect("Invalid URL"),
|
2023-07-07 10:53:31 +02:00
|
|
|
code_verifier: Some(pkce_code_verifier.secret().clone()),
|
|
|
|
},
|
2024-10-26 04:08:48 +02:00
|
|
|
client_id: Some(TEST_INTEGRATION_RS_ID.to_string()),
|
2023-07-07 10:53:31 +02:00
|
|
|
client_secret: None,
|
|
|
|
};
|
|
|
|
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_ENDPOINT))
|
2023-07-07 10:53:31 +02:00
|
|
|
.form(&form_req)
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send code exchange request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2023-07-07 10:53:31 +02:00
|
|
|
assert_no_cache!(response);
|
|
|
|
|
|
|
|
// The body is a json AccessTokenResponse
|
|
|
|
let atr = response
|
|
|
|
.json::<AccessTokenResponse>()
|
|
|
|
.await
|
|
|
|
.expect("Unable to decode AccessTokenResponse");
|
|
|
|
|
|
|
|
// Step 5 - check that the id_token (openid) matches the userinfo endpoint.
|
|
|
|
let oidc_unverified =
|
|
|
|
OidcUnverified::from_str(atr.id_token.as_ref().unwrap()).expect("Failed to parse id_token");
|
|
|
|
|
2023-11-24 03:53:22 +01:00
|
|
|
let oidc = jws_validator
|
|
|
|
.verify(&oidc_unverified)
|
|
|
|
.expect("Failed to verify oidc")
|
|
|
|
.verify_exp(0)
|
|
|
|
.expect("Failed to check exp");
|
2023-07-07 10:53:31 +02:00
|
|
|
|
|
|
|
// This is mostly checked inside of idm/oauth2.rs. This is more to check the oidc
|
|
|
|
// token and the userinfo endpoints.
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(
|
|
|
|
oidc.iss,
|
|
|
|
rsclient.make_url("/oauth2/openid/test_integration")
|
|
|
|
);
|
2023-07-07 10:53:31 +02:00
|
|
|
eprintln!("{:?}", oidc.s_claims.email);
|
2024-10-26 04:08:48 +02:00
|
|
|
assert_eq!(oidc.s_claims.email.as_deref(), Some(NOT_ADMIN_TEST_EMAIL));
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(oidc.s_claims.email_verified, Some(true));
|
2022-04-29 05:03:21 +02:00
|
|
|
|
2024-01-16 01:44:12 +01:00
|
|
|
eprintln!("{:?}", oidc.claims);
|
|
|
|
assert_eq!(
|
|
|
|
oidc.claims.get("test_claim").and_then(|v| v.as_str()),
|
|
|
|
Some("claim_a claim_b")
|
|
|
|
);
|
|
|
|
|
2023-07-09 04:06:40 +02:00
|
|
|
// Check the preflight works.
|
|
|
|
let response = client
|
|
|
|
.request(
|
|
|
|
reqwest::Method::OPTIONS,
|
2023-08-14 12:47:49 +02:00
|
|
|
rsclient.make_url("/oauth2/openid/test_integration/userinfo"),
|
2023-07-09 04:06:40 +02:00
|
|
|
)
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send userinfo preflight request.");
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), reqwest::StatusCode::OK);
|
2023-07-09 04:06:40 +02:00
|
|
|
let cors_header: &str = response
|
|
|
|
.headers()
|
2023-10-14 04:39:14 +02:00
|
|
|
.get(http::header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
2023-07-09 04:06:40 +02:00
|
|
|
.expect("missing access-control-allow-origin header")
|
|
|
|
.to_str()
|
|
|
|
.expect("invalid access-control-allow-origin header");
|
|
|
|
assert!(cors_header.eq("*"));
|
|
|
|
|
2022-04-29 05:03:21 +02:00
|
|
|
let response = client
|
2023-08-14 12:47:49 +02:00
|
|
|
.get(rsclient.make_url("/oauth2/openid/test_integration/userinfo"))
|
2022-04-29 05:03:21 +02:00
|
|
|
.bearer_auth(atr.access_token.clone())
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send userinfo request.");
|
|
|
|
|
|
|
|
let userinfo = response
|
|
|
|
.json::<OidcToken>()
|
|
|
|
.await
|
|
|
|
.expect("Unable to decode OidcToken from userinfo");
|
|
|
|
|
2023-03-07 02:50:45 +01:00
|
|
|
eprintln!("userinfo {userinfo:?}");
|
|
|
|
eprintln!("oidc {oidc:?}");
|
2022-10-17 12:09:47 +02:00
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(userinfo, oidc);
|
2023-07-05 14:26:39 +02:00
|
|
|
|
|
|
|
// auth back with admin so we can test deleting things
|
|
|
|
let res = rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
|
2023-07-05 14:26:39 +02:00
|
|
|
.await;
|
|
|
|
assert!(res.is_ok());
|
|
|
|
rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.idm_oauth2_rs_delete_sup_scope_map(TEST_INTEGRATION_RS_ID, TEST_INTEGRATION_RS_GROUP_ALL)
|
2023-07-05 14:26:39 +02:00
|
|
|
.await
|
|
|
|
.expect("Failed to update oauth2 scopes");
|
|
|
|
}
|
|
|
|
|
2025-01-08 06:41:01 +01:00
|
|
|
/// Test an OAuth 2.0/OpenID public client Authorisation Code flow, with
|
|
|
|
/// `response_mode` unset.
|
|
|
|
///
|
|
|
|
/// The response should be returned as a query parameter.
|
|
|
|
#[kanidmd_testkit::test]
|
|
|
|
async fn test_oauth2_openid_public_flow_mode_unset(rsclient: KanidmClient) {
|
|
|
|
test_oauth2_openid_public_flow_impl(rsclient, None, false).await;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Test an OAuth 2.0/OpenID public client Authorisation Code flow, with
|
|
|
|
/// `response_mode=query`.
|
|
|
|
///
|
|
|
|
/// The response should be returned as a query parameter.
|
|
|
|
#[kanidmd_testkit::test]
|
|
|
|
async fn test_oauth2_openid_public_flow_mode_query(rsclient: KanidmClient) {
|
|
|
|
test_oauth2_openid_public_flow_impl(rsclient, Some("query"), false).await;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Test an OAuth 2.0/OpenID public client Authorisation Code flow, with
|
|
|
|
/// `response_mode=fragment`.
|
|
|
|
///
|
|
|
|
/// The response should be returned in the URI's fragment.
|
|
|
|
#[kanidmd_testkit::test]
|
|
|
|
async fn test_oauth2_openid_public_flow_mode_fragment(rsclient: KanidmClient) {
|
|
|
|
test_oauth2_openid_public_flow_impl(rsclient, Some("fragment"), true).await;
|
|
|
|
}
|
|
|
|
|
2023-07-05 14:26:39 +02:00
|
|
|
#[kanidmd_testkit::test]
|
|
|
|
async fn test_oauth2_token_post_bad_bodies(rsclient: KanidmClient) {
|
|
|
|
let res = rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
|
2023-07-05 14:26:39 +02:00
|
|
|
.await;
|
|
|
|
assert!(res.is_ok());
|
|
|
|
|
|
|
|
let client = reqwest::Client::builder()
|
|
|
|
.redirect(reqwest::redirect::Policy::none())
|
|
|
|
.no_proxy()
|
|
|
|
.build()
|
|
|
|
.expect("Failed to create client.");
|
|
|
|
|
|
|
|
// test for a bad-body request on token
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_ENDPOINT))
|
2023-07-05 14:26:39 +02:00
|
|
|
.form(&serde_json::json!({}))
|
|
|
|
// .bearer_auth(atr.access_token.clone())
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send token request.");
|
|
|
|
println!("{:?}", response);
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
2023-07-05 14:26:39 +02:00
|
|
|
|
|
|
|
// test for a bad-auth request
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_INTROSPECT_ENDPOINT))
|
2023-07-05 14:26:39 +02:00
|
|
|
.form(&serde_json::json!({ "token": "lol" }))
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send token introspection request.");
|
|
|
|
println!("{:?}", response);
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
|
2023-07-05 14:26:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[kanidmd_testkit::test]
|
|
|
|
async fn test_oauth2_token_revoke_post(rsclient: KanidmClient) {
|
|
|
|
let res = rsclient
|
2024-10-26 04:08:48 +02:00
|
|
|
.auth_simple_password(ADMIN_TEST_USER, ADMIN_TEST_PASSWORD)
|
2023-07-05 14:26:39 +02:00
|
|
|
.await;
|
|
|
|
assert!(res.is_ok());
|
|
|
|
|
|
|
|
let client = reqwest::Client::builder()
|
|
|
|
.redirect(reqwest::redirect::Policy::none())
|
|
|
|
.no_proxy()
|
|
|
|
.build()
|
|
|
|
.expect("Failed to create client.");
|
|
|
|
|
|
|
|
// test for a bad-body request on token
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_REVOKE_ENDPOINT))
|
2023-07-05 14:26:39 +02:00
|
|
|
.form(&serde_json::json!({}))
|
|
|
|
.bearer_auth("lolol")
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send token request.");
|
|
|
|
println!("{:?}", response);
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
2023-07-05 14:26:39 +02:00
|
|
|
|
|
|
|
// test for a invalid format request on token
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_REVOKE_ENDPOINT))
|
2023-07-05 14:26:39 +02:00
|
|
|
.json("")
|
|
|
|
.bearer_auth("lolol")
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send token request.");
|
|
|
|
println!("{:?}", response);
|
|
|
|
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
2023-07-05 14:26:39 +02:00
|
|
|
|
|
|
|
// test for a bad-body request on token
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_REVOKE_ENDPOINT))
|
2023-07-05 14:26:39 +02:00
|
|
|
.form(&serde_json::json!({}))
|
|
|
|
.bearer_auth("Basic lolol")
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send token request.");
|
|
|
|
println!("{:?}", response);
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
2023-07-05 14:26:39 +02:00
|
|
|
|
|
|
|
// test for a bad-body request on token
|
|
|
|
let response = client
|
2024-10-26 04:08:48 +02:00
|
|
|
.post(rsclient.make_url(OAUTH2_TOKEN_REVOKE_ENDPOINT))
|
2023-07-05 14:26:39 +02:00
|
|
|
.body(serde_json::json!({}).to_string())
|
|
|
|
.bearer_auth("Basic lolol")
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.expect("Failed to send token request.");
|
|
|
|
println!("{:?}", response);
|
2024-08-26 01:30:20 +02:00
|
|
|
assert_eq!(response.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
|
2021-06-29 06:23:39 +02:00
|
|
|
}
|