kanidm/server/core/src/https/oauth2.rs
2024-09-05 04:19:27 +00:00

783 lines
28 KiB
Rust

use super::errors::WebError;
use super::middleware::KOpId;
use super::ServerState;
use crate::https::extractors::VerifiedClientInformation;
use axum::{
body::Body,
extract::{Path, Query, State},
http::header::{
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, LOCATION,
WWW_AUTHENTICATE,
},
http::{HeaderValue, StatusCode},
middleware::from_fn,
response::{IntoResponse, Response},
routing::{get, post},
Extension, Form, Json, Router,
};
use axum_macros::debug_handler;
use compact_jwt::{JwkKeySet, OidcToken};
use kanidm_proto::constants::uri::{
OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT, OAUTH2_AUTHORISE_REJECT,
};
use kanidm_proto::constants::APPLICATION_JSON;
use kanidm_proto::oauth2::AuthorisationResponse;
use kanidmd_lib::idm::oauth2::{
AccessTokenIntrospectRequest, AccessTokenRequest, AuthorisationRequest, AuthorisePermitSuccess,
AuthoriseResponse, ErrorResponse, Oauth2Error, TokenRevokeRequest,
};
use kanidmd_lib::prelude::f_eq;
use kanidmd_lib::prelude::*;
use kanidmd_lib::value::PartialValue;
use serde::{Deserialize, Serialize};
// TODO: merge this into a value in WebError later
pub struct HTTPOauth2Error(Oauth2Error);
impl IntoResponse for HTTPOauth2Error {
fn into_response(self) -> Response {
let HTTPOauth2Error(error) = self;
if let Oauth2Error::AuthenticationRequired = error {
(
StatusCode::UNAUTHORIZED,
[
(WWW_AUTHENTICATE, "Bearer"),
(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
],
)
.into_response()
} else {
let err = ErrorResponse {
error: error.to_string(),
..Default::default()
};
let body = match serde_json::to_string(&err) {
Ok(val) => val,
Err(e) => {
admin_warn!("Failed to serialize error response: original_error=\"{:?}\" serialization_error=\"{:?}\"", err, e);
format!("{:?}", err)
}
};
(
StatusCode::BAD_REQUEST,
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
body,
)
.into_response()
}
}
}
// == Oauth2 Configuration Endpoints ==
/// Get a filter matching a given OAuth2 Resource Server
pub(crate) fn oauth2_id(rs_name: &str) -> Filter<FilterInvalid> {
filter_all!(f_and!([
f_eq(Attribute::Class, EntryClass::OAuth2ResourceServer.into()),
f_eq(Attribute::Name, PartialValue::new_iname(rs_name))
]))
}
#[utoipa::path(
get,
path = "/ui/images/oauth2/{rs_name}",
operation_id = "oauth2_image_get",
responses(
(status = 200, description = "Ok", body=&[u8]),
(status = 401, description = "Authorization required"),
(status = 403, description = "Not Authorized"),
),
security(("token_jwt" = [])),
tag = "ui",
)]
/// This returns the image for the OAuth2 Resource Server if the user has permissions
///
pub(crate) async fn oauth2_image_get(
State(state): State<ServerState>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Path(rs_name): Path<String>,
) -> Response {
let rs_filter = oauth2_id(&rs_name);
let res = state
.qe_r_ref
.handle_oauth2_rs_image_get_image(client_auth_info, rs_filter)
.await;
match res {
Ok(Some(image)) => (
StatusCode::OK,
[(CONTENT_TYPE, image.filetype.as_content_type_str())],
image.contents,
)
.into_response(),
Ok(None) => {
warn!(?rs_name, "No image set for oauth2 client");
(StatusCode::NOT_FOUND, "").into_response()
}
Err(err) => {
error!(?err, "Unable to get image for oauth2 client");
// TODO: a 404 probably isn't perfect but it's not the worst
(StatusCode::INTERNAL_SERVER_ERROR, "").into_response()
}
}
}
// == OAUTH2 PROTOCOL FLOW HANDLERS ==
//
// oauth2 (partial)
// https://tools.ietf.org/html/rfc6749
// oauth2 pkce
// https://tools.ietf.org/html/rfc7636
//
// TODO
// oauth2 token introspection
// https://tools.ietf.org/html/rfc7662
// oauth2 bearer token
// https://tools.ietf.org/html/rfc6750
//
// From https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
//
// +----------+
// | Resource |
// | Owner |
// | |
// +----------+
// ^
// |
// (B)
// +----|-----+ Client Identifier +---------------+
// | -+----(A)-- & Redirection URI ---->| |
// | User- | | Authorization |
// | Agent -+----(B)-- User authenticates --->| Server |
// | | | |
// | -+----(C)-- Authorization Code ---<| |
// +-|----|---+ +---------------+
// | | ^ v
// (A) (C) | |
// | | | |
// ^ v | |
// +---------+ | |
// | |>---(D)-- Authorization Code ---------' |
// | Client | & Redirection URI |
// | | |
// | |<---(E)----- Access Token -------------------'
// +---------+ (w/ Optional Refresh Token)
//
// Note: The lines illustrating steps (A), (B), and (C) are broken into
// two parts as they pass through the user-agent.
//
// In this diagram, kanidm is the authorisation server. Each step is handled by:
//
// * Client Identifier A) oauth2_authorise_get
// * User authenticates B) normal kanidm auth flow
// * Authorization Code C) oauth2_authorise_permit_get
// oauth2_authorise_reject_get
// * Authorization Code / Access Token
// D/E) oauth2_token_post
//
// These functions appear stateless, but the state is managed through encrypted
// tokens transmitted in the responses of this flow. This is because in a HA setup
// we can not guarantee that the User-Agent or the Resource Server (client) will
// access the same Kanidm instance, and we can not rely on replication in these
// cases. As a result, we must have our state in localised tokens so that any
// valid Kanidm instance in the topology can handle these request.
//
#[instrument(level = "debug", skip(state, kopid))]
pub async fn oauth2_authorise_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Json(auth_req): Json<AuthorisationRequest>,
) -> impl IntoResponse {
let mut res = oauth2_authorise(state, auth_req, kopid, client_auth_info)
.await
.into_response();
if res.status() == StatusCode::FOUND {
// in post, we need the redirect not to be issued, so we mask 302 to 200
*res.status_mut() = StatusCode::OK;
}
res
}
#[instrument(level = "debug", skip(state, kopid))]
pub async fn oauth2_authorise_get(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Query(auth_req): Query<AuthorisationRequest>,
) -> impl IntoResponse {
// Start the oauth2 authorisation flow to present to the user.
oauth2_authorise(state, auth_req, kopid, client_auth_info).await
}
async fn oauth2_authorise(
state: ServerState,
auth_req: AuthorisationRequest,
kopid: KOpId,
client_auth_info: ClientAuthInfo,
) -> impl IntoResponse {
let res: Result<AuthoriseResponse, Oauth2Error> = state
.qe_r_ref
.handle_oauth2_authorise(client_auth_info, auth_req, kopid.eventid)
.await;
match res {
Ok(AuthoriseResponse::ConsentRequested {
client_name,
scopes,
pii_scopes,
consent_token,
}) => {
// Render a redirect to the consent page for the user to interact with
// to authorise this session-id
// This is json so later we can expand it with better detail.
#[allow(clippy::unwrap_used)]
let body = serde_json::to_string(&AuthorisationResponse::ConsentRequested {
client_name,
scopes,
pii_scopes,
consent_token,
})
.unwrap();
#[allow(clippy::unwrap_used)]
Response::builder()
.status(StatusCode::OK)
.body(body.into())
.unwrap()
}
Ok(AuthoriseResponse::Permitted(AuthorisePermitSuccess {
mut redirect_uri,
state,
code,
})) => {
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11
// We could consider changing this to 303?
#[allow(clippy::unwrap_used)]
let body =
Body::from(serde_json::to_string(&AuthorisationResponse::Permitted).unwrap());
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("state", &state)
.append_pair("code", &code);
#[allow(clippy::unwrap_used)]
Response::builder()
.status(StatusCode::FOUND)
.header(
LOCATION,
HeaderValue::from_str(redirect_uri.as_str()).unwrap(),
)
// I think the client server needs this
.header(
ACCESS_CONTROL_ALLOW_ORIGIN,
HeaderValue::from_str(&redirect_uri.origin().ascii_serialization()).unwrap(),
)
.body(body)
.unwrap()
}
Err(Oauth2Error::AuthenticationRequired) => {
// This will trigger our ui to auth and retry.
#[allow(clippy::unwrap_used)]
Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header(WWW_AUTHENTICATE, HeaderValue::from_static("Bearer"))
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(Body::empty())
.unwrap()
}
Err(Oauth2Error::AccessDenied) => {
// If scopes are not available for this account.
#[allow(clippy::unwrap_used)]
Response::builder()
.status(StatusCode::FORBIDDEN)
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(Body::empty())
.unwrap()
}
/*
RFC - If the request fails due to a missing, invalid, or mismatching
redirection URI, or if the client identifier is missing or invalid,
the authorization server SHOULD inform the resource owner of the
error and MUST NOT automatically redirect the user-agent to the
invalid redirection URI.
*/
// To further this, it appears that a malicious client configuration can set a phishing
// site as the redirect URL, and then use that to trigger certain types of attacks. Instead
// we do NOT redirect in an error condition, and just render the error ourselves.
Err(e) => {
admin_error!(
"Unable to authorise - Error ID: {:?} error: {}",
kopid.eventid,
&e.to_string()
);
#[allow(clippy::unwrap_used)]
Response::builder()
.status(StatusCode::BAD_REQUEST)
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(Body::empty())
.unwrap()
}
}
}
pub async fn oauth2_authorise_permit_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Json(consent_req): Json<String>,
) -> impl IntoResponse {
let mut res = oauth2_authorise_permit(state, consent_req, kopid, client_auth_info)
.await
.into_response();
if res.status() == StatusCode::FOUND {
// in post, we need the redirect not to be issued, so we mask 302 to 200
*res.status_mut() = StatusCode::OK;
}
res
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ConsentRequestData {
token: String,
}
pub async fn oauth2_authorise_permit_get(
State(state): State<ServerState>,
Query(token): Query<ConsentRequestData>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
) -> impl IntoResponse {
// When this is called, this indicates consent to proceed from the user.
oauth2_authorise_permit(state, token.token, kopid, client_auth_info).await
}
async fn oauth2_authorise_permit(
state: ServerState,
consent_req: String,
kopid: KOpId,
client_auth_info: ClientAuthInfo,
) -> impl IntoResponse {
let res = state
.qe_w_ref
.handle_oauth2_authorise_permit(client_auth_info, consent_req, kopid.eventid)
.await;
match res {
Ok(AuthorisePermitSuccess {
mut redirect_uri,
state,
code,
}) => {
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.11
// We could consider changing this to 303?
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("state", &state)
.append_pair("code", &code);
#[allow(clippy::unwrap_used)]
Response::builder()
.status(StatusCode::FOUND)
.header(LOCATION, redirect_uri.as_str())
.header(
ACCESS_CONTROL_ALLOW_ORIGIN,
redirect_uri.origin().ascii_serialization(),
)
.body(Body::empty())
.unwrap()
}
Err(_e) => {
// If an error happens in our consent flow, I think
// that we should NOT redirect to the calling application
// and we need to handle that locally somehow.
// This needs to be better!
//
// Turns out this instinct was correct:
// https://www.proofpoint.com/us/blog/cloud-security/microsoft-and-github-oauth-implementation-vulnerabilities-lead-redirection
// Possible to use this with a malicious client configuration to phish / spam.
#[allow(clippy::unwrap_used)]
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(Body::empty())
.unwrap()
}
}
}
// When this is called, this indicates the user has REJECTED the intent to proceed.
pub async fn oauth2_authorise_reject_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Form(consent_req): Form<ConsentRequestData>,
) -> Response<Body> {
oauth2_authorise_reject(state, consent_req.token, kopid, client_auth_info).await
}
pub async fn oauth2_authorise_reject_get(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Query(consent_req): Query<ConsentRequestData>,
) -> Response<Body> {
oauth2_authorise_reject(state, consent_req.token, kopid, client_auth_info).await
}
// // https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
// // If the user willingly rejects the authorisation, we must redirect
// // with an error.
async fn oauth2_authorise_reject(
state: ServerState,
consent_req: String,
kopid: KOpId,
client_auth_info: ClientAuthInfo,
) -> Response<Body> {
// Need to go back to the redir_uri
// For this, we'll need to lookup where to go.
let res = state
.qe_r_ref
.handle_oauth2_authorise_reject(client_auth_info, consent_req, kopid.eventid)
.await;
match res {
Ok(mut redirect_uri) => {
redirect_uri
.query_pairs_mut()
.clear()
.append_pair("error", "access_denied")
.append_pair("error_description", "authorisation rejected");
#[allow(clippy::unwrap_used)]
Response::builder()
.header(LOCATION, redirect_uri.as_str())
.header(
ACCESS_CONTROL_ALLOW_ORIGIN,
redirect_uri.origin().ascii_serialization(),
)
.body(Body::empty())
.unwrap()
// I think the client server needs this
}
Err(_e) => {
// If an error happens in our reject flow, I think
// that we should NOT redirect to the calling application
// and we need to handle that locally somehow.
// This needs to be better!
#[allow(clippy::unwrap_used)]
Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(Body::empty())
.unwrap()
}
}
}
#[axum_macros::debug_handler]
#[instrument(skip(state, kopid, client_auth_info), level = "DEBUG")]
pub async fn oauth2_token_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Form(tok_req): Form<AccessTokenRequest>,
) -> impl IntoResponse {
// This is called directly by the resource server, where we then issue
// the token to the caller.
// Do we change the method/path we take here based on the type of requested
// grant? Should we cease the delayed/async session update here and just opt
// for a wr txn?
match state
.qe_w_ref
.handle_oauth2_token_exchange(client_auth_info, tok_req, kopid.eventid)
.await
{
Ok(tok_res) => (
StatusCode::OK,
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
Json(tok_res),
)
.into_response(),
Err(e) => HTTPOauth2Error(e).into_response(),
}
}
// // For future openid integration
pub async fn oauth2_openid_discovery_get(
State(state): State<ServerState>,
Path(client_id): Path<String>,
Extension(kopid): Extension<KOpId>,
) -> impl IntoResponse {
let res = state
.qe_r_ref
.handle_oauth2_openid_discovery(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()
}
}
}
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>,
Path(client_id): Path<String>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
) -> Result<Json<OidcToken>, HTTPOauth2Error> {
// The token we want to inspect is in the authorisation header.
let client_token = match client_auth_info.bearer_token {
Some(val) => val,
None => {
error!("Bearer Authentication Not Provided");
return Err(HTTPOauth2Error(Oauth2Error::AuthenticationRequired));
}
};
let res = state
.qe_r_ref
.handle_oauth2_openid_userinfo(client_id, client_token, kopid.eventid)
.await;
match res {
Ok(uir) => Ok(Json(uir)),
Err(e) => Err(HTTPOauth2Error(e)),
}
}
pub async fn oauth2_openid_publickey_get(
State(state): State<ServerState>,
Path(client_id): Path<String>,
Extension(kopid): Extension<KOpId>,
) -> Result<Json<JwkKeySet>, WebError> {
state
.qe_r_ref
.handle_oauth2_openid_publickey(client_id, kopid.eventid)
.await
.map(Json::from)
.map_err(WebError::from)
}
/// This is called directly by the resource server, where we then issue
/// information about this token to the caller.
pub async fn oauth2_token_introspect_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Form(intr_req): Form<AccessTokenIntrospectRequest>,
) -> impl IntoResponse {
request_trace!("Introspect Request - {:?}", intr_req);
let res = state
.qe_r_ref
.handle_oauth2_token_introspect(client_auth_info, intr_req, kopid.eventid)
.await;
match res {
Ok(atr) => {
let body = match serde_json::to_string(&atr) {
Ok(val) => val,
Err(e) => {
admin_warn!("Failed to serialize introspect response: original_data=\"{:?}\" serialization_error=\"{:?}\"", atr, e);
format!("{:?}", atr)
}
};
#[allow(clippy::unwrap_used)]
Response::builder()
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.header(CONTENT_TYPE, APPLICATION_JSON)
.body(Body::from(body))
.unwrap()
}
Err(Oauth2Error::AuthenticationRequired) => {
// This will trigger our ui to auth and retry.
#[allow(clippy::unwrap_used)]
Response::builder()
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.status(StatusCode::UNAUTHORIZED)
.body(Body::empty())
.unwrap()
}
Err(e) => {
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
let err = ErrorResponse {
error: e.to_string(),
..Default::default()
};
let body = match serde_json::to_string(&err) {
Ok(val) => val,
Err(e) => {
format!("{:?}", e)
}
};
#[allow(clippy::unwrap_used)]
Response::builder()
.status(StatusCode::BAD_REQUEST)
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
.body(Body::from(body))
.unwrap()
}
}
}
/// This is called directly by the resource server, where we then revoke
/// the token identified by this request.
pub async fn oauth2_token_revoke_post(
State(state): State<ServerState>,
Extension(kopid): Extension<KOpId>,
VerifiedClientInformation(client_auth_info): VerifiedClientInformation,
Form(intr_req): Form<TokenRevokeRequest>,
) -> impl IntoResponse {
request_trace!("Revoke Request - {:?}", intr_req);
let res = state
.qe_w_ref
.handle_oauth2_token_revoke(client_auth_info, intr_req, kopid.eventid)
.await;
match res {
Ok(()) => (StatusCode::OK, [(ACCESS_CONTROL_ALLOW_ORIGIN, "*")], "").into_response(),
Err(Oauth2Error::AuthenticationRequired) => {
// This will trigger our ui to auth and retry.
(
StatusCode::UNAUTHORIZED,
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
"",
)
.into_response()
}
Err(e) => {
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
let err = ErrorResponse {
error: e.to_string(),
..Default::default()
};
(
StatusCode::BAD_REQUEST,
[(ACCESS_CONTROL_ALLOW_ORIGIN, "*")],
serde_json::to_string(&err).unwrap_or("".to_string()),
)
.into_response()
}
}
}
// Some requests from browsers require preflight so that CORS works.
pub async fn oauth2_preflight_options() -> Response {
(
StatusCode::OK,
[
(ACCESS_CONTROL_ALLOW_ORIGIN, "*"),
(ACCESS_CONTROL_ALLOW_HEADERS, "Authorization"),
],
String::new(),
)
.into_response()
}
pub fn route_setup(state: ServerState) -> Router<ServerState> {
// this has all the openid-related routes
let openid_router = Router::new()
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
// // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
"/oauth2/openid/:client_id/.well-known/openid-configuration",
get(oauth2_openid_discovery_get).options(oauth2_preflight_options),
)
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
// // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
"/oauth2/openid/:client_id/userinfo",
get(oauth2_openid_userinfo_get).options(oauth2_preflight_options),
)
// // ⚠️ ⚠️ WARNING ⚠️ ⚠️
// // IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
"/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()
.route("/oauth2", get(super::v1_oauth2::oauth2_get))
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
OAUTH2_AUTHORISE,
post(oauth2_authorise_post).get(oauth2_authorise_get),
)
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
OAUTH2_AUTHORISE_PERMIT,
post(oauth2_authorise_permit_post).get(oauth2_authorise_permit_get),
)
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
OAUTH2_AUTHORISE_REJECT,
post(oauth2_authorise_reject_post).get(oauth2_authorise_reject_get),
)
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
"/oauth2/token",
post(oauth2_token_post).options(oauth2_preflight_options),
)
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
.route(
"/oauth2/token/introspect",
post(oauth2_token_introspect_post),
)
.route("/oauth2/token/revoke", post(oauth2_token_revoke_post))
.merge(openid_router)
.with_state(state)
.layer(from_fn(super::middleware::caching::dont_cache_me))
}