#![warn(missing_docs)]
#![allow(clippy::unreadable_literal, clippy::type_complexity)]
#![cfg_attr(test, allow(clippy::cognitive_complexity))]
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
#[macro_use]
extern crate serde_derive;
use oauth2::ResponseType as OAuth2ResponseType;
use url::Url;
use std::borrow::Cow;
use std::marker::PhantomData;
use std::str;
use std::time::Duration;
pub use oauth2::{
AccessToken, AuthType, AuthUrl, AuthorizationCode, ClientCredentialsTokenRequest, ClientId,
ClientSecret, CodeTokenRequest, ConfigurationError, CsrfToken, EmptyExtraTokenFields,
ErrorResponse, ErrorResponseType, ExtraTokenFields, HttpRequest, HttpResponse,
IntrospectionRequest, IntrospectionUrl, PasswordTokenRequest, PkceCodeChallenge,
PkceCodeChallengeMethod, PkceCodeVerifier, RedirectUrl, RefreshToken, RefreshTokenRequest,
RequestTokenError, ResourceOwnerPassword, ResourceOwnerUsername, RevocableToken,
RevocationErrorResponseType, RevocationRequest, RevocationUrl, Scope, StandardErrorResponse,
StandardTokenIntrospectionResponse, StandardTokenResponse, TokenIntrospectionResponse,
TokenResponse as OAuth2TokenResponse, TokenType, TokenUrl,
};
pub use oauth2::http;
pub use oauth2::url;
#[cfg(all(feature = "curl", not(target_arch = "wasm32")))]
pub use oauth2::curl;
#[cfg(all(feature = "curl", target_arch = "wasm32"))]
compile_error!("wasm32 is not supported with the `curl` feature. Use the `reqwest` backend or a custom backend for wasm32 support");
#[cfg(feature = "reqwest")]
pub use oauth2::reqwest;
#[cfg(feature = "ureq")]
pub use oauth2::ureq;
pub use claims::{
AdditionalClaims, AddressClaim, EmptyAdditionalClaims, GenderClaim, StandardClaims,
};
pub use discovery::{
AdditionalProviderMetadata, DiscoveryError, EmptyAdditionalProviderMetadata, ProviderMetadata,
};
pub use id_token::IdTokenFields;
pub use id_token::{IdToken, IdTokenClaims};
pub use jwt::JsonWebTokenError;
use jwt::{JsonWebToken, JsonWebTokenAccess, JsonWebTokenAlgorithm, JsonWebTokenHeader};
pub use types::{
AccessTokenHash, AddressCountry, AddressLocality, AddressPostalCode, AddressRegion,
ApplicationType, Audience, AuthDisplay, AuthPrompt, AuthenticationContextClass,
AuthenticationMethodReference, AuthorizationCodeHash, ClaimName, ClaimType, ClientAuthMethod,
ClientConfigUrl, ClientContactEmail, ClientName, ClientUrl, EndUserBirthday, EndUserEmail,
EndUserFamilyName, EndUserGivenName, EndUserMiddleName, EndUserName, EndUserNickname,
EndUserPhoneNumber, EndUserPictureUrl, EndUserProfileUrl, EndUserTimezone, EndUserUsername,
EndUserWebsiteUrl, FormattedAddress, GrantType, InitiateLoginUrl, IssuerUrl, JsonWebKey,
JsonWebKeyId, JsonWebKeySet, JsonWebKeySetUrl, JsonWebKeyType, JsonWebKeyUse,
JweContentEncryptionAlgorithm, JweKeyManagementAlgorithm, JwsSigningAlgorithm, LanguageTag,
LocalizedClaim, LoginHint, LogoUrl, Nonce, OpPolicyUrl, OpTosUrl, PolicyUrl, PrivateSigningKey,
RegistrationAccessToken, RegistrationUrl, RequestUrl, ResponseMode, ResponseType,
ResponseTypes, SectorIdentifierUrl, ServiceDocUrl, SigningError, StreetAddress,
SubjectIdentifier, SubjectIdentifierType, ToSUrl,
};
pub use user_info::{
UserInfoClaims, UserInfoError, UserInfoJsonWebToken, UserInfoRequest, UserInfoUrl,
};
use verification::{AudiencesClaim, IssuerClaim};
pub use verification::{
ClaimsVerificationError, IdTokenVerifier, NonceVerifier, SignatureVerificationError,
UserInfoVerifier,
};
#[macro_use]
mod macros;
pub mod core;
pub mod registration;
mod claims;
mod discovery;
mod helpers;
mod id_token;
pub(crate) mod types;
mod user_info;
mod verification;
mod http_utils;
mod jwt;
const CONFIG_URL_SUFFIX: &str = ".well-known/openid-configuration";
const OPENID_SCOPE: &str = "openid";
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum AuthenticationFlow<RT: ResponseType> {
AuthorizationCode,
Implicit(bool),
Hybrid(Vec<RT>),
}
#[derive(Clone, Debug)]
pub struct Client<AC, AD, GC, JE, JS, JT, JU, K, P, TE, TR, TT, TIR, RT, TRE>
where
AC: AdditionalClaims,
AD: AuthDisplay,
GC: GenderClaim,
JE: JweContentEncryptionAlgorithm<JT>,
JS: JwsSigningAlgorithm<JT>,
JT: JsonWebKeyType,
JU: JsonWebKeyUse,
K: JsonWebKey<JS, JT, JU>,
P: AuthPrompt,
TE: ErrorResponse,
TR: TokenResponse<AC, GC, JE, JS, JT, TT>,
TT: TokenType + 'static,
TIR: TokenIntrospectionResponse<TT>,
RT: RevocableToken,
TRE: ErrorResponse,
{
oauth2_client: oauth2::Client<TE, TR, TT, TIR, RT, TRE>,
client_id: ClientId,
client_secret: Option<ClientSecret>,
issuer: IssuerUrl,
userinfo_endpoint: Option<UserInfoUrl>,
jwks: JsonWebKeySet<JS, JT, JU, K>,
id_token_signing_algs: Vec<JS>,
use_openid_scope: bool,
_phantom: PhantomData<(AC, AD, GC, JE, P)>,
}
impl<AC, AD, GC, JE, JS, JT, JU, K, P, TE, TR, TT, TIR, RT, TRE>
Client<AC, AD, GC, JE, JS, JT, JU, K, P, TE, TR, TT, TIR, RT, TRE>
where
AC: AdditionalClaims,
AD: AuthDisplay,
GC: GenderClaim,
JE: JweContentEncryptionAlgorithm<JT>,
JS: JwsSigningAlgorithm<JT>,
JT: JsonWebKeyType,
JU: JsonWebKeyUse,
K: JsonWebKey<JS, JT, JU>,
P: AuthPrompt,
TE: ErrorResponse + 'static,
TR: TokenResponse<AC, GC, JE, JS, JT, TT>,
TT: TokenType + 'static,
TIR: TokenIntrospectionResponse<TT>,
RT: RevocableToken,
TRE: ErrorResponse + 'static,
{
pub fn new(
client_id: ClientId,
client_secret: Option<ClientSecret>,
issuer: IssuerUrl,
auth_url: AuthUrl,
token_url: Option<TokenUrl>,
userinfo_endpoint: Option<UserInfoUrl>,
jwks: JsonWebKeySet<JS, JT, JU, K>,
) -> Self {
Client {
oauth2_client: oauth2::Client::new(
client_id.clone(),
client_secret.clone(),
auth_url,
token_url,
),
client_id,
client_secret,
issuer,
userinfo_endpoint,
jwks,
id_token_signing_algs: vec![],
use_openid_scope: true,
_phantom: PhantomData,
}
}
pub fn from_provider_metadata<A, CA, CN, CT, G, JK, RM, RS, S>(
provider_metadata: ProviderMetadata<A, AD, CA, CN, CT, G, JE, JK, JS, JT, JU, K, RM, RS, S>,
client_id: ClientId,
client_secret: Option<ClientSecret>,
) -> Self
where
A: AdditionalProviderMetadata,
CA: ClientAuthMethod,
CN: ClaimName,
CT: ClaimType,
G: GrantType,
JK: JweKeyManagementAlgorithm,
RM: ResponseMode,
RS: ResponseType,
S: SubjectIdentifierType,
{
Client {
oauth2_client: oauth2::Client::new(
client_id.clone(),
client_secret.clone(),
provider_metadata.authorization_endpoint().clone(),
provider_metadata.token_endpoint().cloned(),
),
client_id,
client_secret,
issuer: provider_metadata.issuer().clone(),
userinfo_endpoint: provider_metadata.userinfo_endpoint().cloned(),
jwks: provider_metadata.jwks().to_owned(),
id_token_signing_algs: provider_metadata
.id_token_signing_alg_values_supported()
.to_owned(),
use_openid_scope: true,
_phantom: PhantomData,
}
}
pub fn set_auth_type(mut self, auth_type: AuthType) -> Self {
self.oauth2_client = self.oauth2_client.set_auth_type(auth_type);
self
}
pub fn set_redirect_uri(mut self, redirect_url: RedirectUrl) -> Self {
self.oauth2_client = self.oauth2_client.set_redirect_uri(redirect_url);
self
}
pub fn set_introspection_uri(mut self, introspection_url: IntrospectionUrl) -> Self {
self.oauth2_client = self.oauth2_client.set_introspection_uri(introspection_url);
self
}
pub fn set_revocation_uri(mut self, revocation_url: RevocationUrl) -> Self {
self.oauth2_client = self.oauth2_client.set_revocation_uri(revocation_url);
self
}
pub fn enable_openid_scope(mut self) -> Self {
self.use_openid_scope = true;
self
}
pub fn disable_openid_scope(mut self) -> Self {
self.use_openid_scope = false;
self
}
pub fn id_token_verifier(&self) -> IdTokenVerifier<JS, JT, JU, K> {
let verifier = if let Some(ref client_secret) = self.client_secret {
IdTokenVerifier::new_confidential_client(
self.client_id.clone(),
client_secret.clone(),
self.issuer.clone(),
self.jwks.clone(),
)
} else {
IdTokenVerifier::new_public_client(
self.client_id.clone(),
self.issuer.clone(),
self.jwks.clone(),
)
};
verifier.set_allowed_algs(self.id_token_signing_algs.clone())
}
pub fn authorize_url<NF, RS, SF>(
&self,
authentication_flow: AuthenticationFlow<RS>,
state_fn: SF,
nonce_fn: NF,
) -> AuthorizationRequest<AD, P, RS>
where
NF: FnOnce() -> Nonce + 'static,
RS: ResponseType,
SF: FnOnce() -> CsrfToken + 'static,
{
let request = AuthorizationRequest {
inner: self.oauth2_client.authorize_url(state_fn),
acr_values: Vec::new(),
authentication_flow,
claims_locales: Vec::new(),
display: None,
id_token_hint: None,
login_hint: None,
max_age: None,
nonce: nonce_fn(),
prompts: Vec::new(),
ui_locales: Vec::new(),
};
if self.use_openid_scope {
request.add_scope(Scope::new(OPENID_SCOPE.to_string()))
} else {
request
}
}
pub fn exchange_code(&self, code: AuthorizationCode) -> CodeTokenRequest<TE, TR, TT> {
self.oauth2_client.exchange_code(code)
}
pub fn exchange_refresh_token<'a, 'b>(
&'a self,
refresh_token: &'b RefreshToken,
) -> RefreshTokenRequest<'b, TE, TR, TT>
where
'a: 'b,
{
self.oauth2_client.exchange_refresh_token(refresh_token)
}
pub fn exchange_password<'a, 'b>(
&'a self,
username: &'b ResourceOwnerUsername,
password: &'b ResourceOwnerPassword,
) -> PasswordTokenRequest<'b, TE, TR, TT>
where
'a: 'b,
{
self.oauth2_client.exchange_password(username, password)
}
pub fn exchange_client_credentials<'a, 'b>(
&'a self,
) -> ClientCredentialsTokenRequest<'b, TE, TR, TT>
where
'a: 'b,
{
self.oauth2_client.exchange_client_credentials()
}
pub fn user_info(
&self,
access_token: AccessToken,
expected_subject: Option<SubjectIdentifier>,
) -> Result<UserInfoRequest<JE, JS, JT, JU, K>, ConfigurationError> {
Ok(UserInfoRequest {
url: self
.userinfo_endpoint
.as_ref()
.ok_or(ConfigurationError::MissingUrl("userinfo"))?,
access_token,
require_signed_response: false,
signed_response_verifier: UserInfoVerifier::new(
self.client_id.clone(),
self.issuer.clone(),
self.jwks.clone(),
expected_subject,
),
})
}
pub fn introspect<'a>(
&'a self,
token: &'a AccessToken,
) -> Result<IntrospectionRequest<'a, TE, TIR, TT>, ConfigurationError> {
self.oauth2_client.introspect(token)
}
pub fn revoke_token(
&self,
token: RT,
) -> Result<RevocationRequest<RT, TRE>, ConfigurationError> {
self.oauth2_client.revoke_token(token)
}
}
pub struct AuthorizationRequest<'a, AD, P, RT>
where
AD: AuthDisplay,
P: AuthPrompt,
RT: ResponseType,
{
inner: oauth2::AuthorizationRequest<'a>,
acr_values: Vec<AuthenticationContextClass>,
authentication_flow: AuthenticationFlow<RT>,
claims_locales: Vec<LanguageTag>,
display: Option<AD>,
id_token_hint: Option<String>,
login_hint: Option<LoginHint>,
max_age: Option<Duration>,
nonce: Nonce,
prompts: Vec<P>,
ui_locales: Vec<LanguageTag>,
}
impl<'a, AD, P, RT> AuthorizationRequest<'a, AD, P, RT>
where
AD: AuthDisplay,
P: AuthPrompt,
RT: ResponseType,
{
pub fn add_scope(mut self, scope: Scope) -> Self {
self.inner = self.inner.add_scope(scope);
self
}
pub fn add_extra_param<N, V>(mut self, name: N, value: V) -> Self
where
N: Into<Cow<'a, str>>,
V: Into<Cow<'a, str>>,
{
self.inner = self.inner.add_extra_param(name, value);
self
}
pub fn set_pkce_challenge(mut self, pkce_code_challenge: PkceCodeChallenge) -> Self {
self.inner = self.inner.set_pkce_challenge(pkce_code_challenge);
self
}
pub fn add_auth_context_value(mut self, acr_value: AuthenticationContextClass) -> Self {
self.acr_values.push(acr_value);
self
}
pub fn add_claims_locale(mut self, claims_locale: LanguageTag) -> Self {
self.claims_locales.push(claims_locale);
self
}
pub fn set_display(mut self, display: AD) -> Self {
self.display = Some(display);
self
}
pub fn set_id_token_hint<AC, GC, JE, JS, JT>(
mut self,
id_token_hint: &'a IdToken<AC, GC, JE, JS, JT>,
) -> Self
where
AC: AdditionalClaims,
GC: GenderClaim,
JE: JweContentEncryptionAlgorithm<JT>,
JS: JwsSigningAlgorithm<JT>,
JT: JsonWebKeyType,
{
self.id_token_hint = Some(id_token_hint.to_string());
self
}
pub fn set_login_hint(mut self, login_hint: LoginHint) -> Self {
self.login_hint = Some(login_hint);
self
}
pub fn set_max_age(mut self, max_age: Duration) -> Self {
self.max_age = Some(max_age);
self
}
pub fn add_prompt(mut self, prompt: P) -> Self {
self.prompts.push(prompt);
self
}
pub fn add_ui_locale(mut self, ui_locale: LanguageTag) -> Self {
self.ui_locales.push(ui_locale);
self
}
pub fn set_redirect_uri(mut self, redirect_url: Cow<'a, RedirectUrl>) -> Self {
self.inner = self.inner.set_redirect_uri(redirect_url);
self
}
pub fn url(self) -> (Url, CsrfToken, Nonce) {
let response_type = match self.authentication_flow {
AuthenticationFlow::AuthorizationCode => core::CoreResponseType::Code.to_oauth2(),
AuthenticationFlow::Implicit(include_token) => {
if include_token {
OAuth2ResponseType::new(
vec![
core::CoreResponseType::IdToken,
core::CoreResponseType::Token,
]
.iter()
.map(|response_type| response_type.as_ref())
.collect::<Vec<_>>()
.join(" "),
)
} else {
core::CoreResponseType::IdToken.to_oauth2()
}
}
AuthenticationFlow::Hybrid(ref response_types) => OAuth2ResponseType::new(
response_types
.iter()
.map(|response_type| response_type.as_ref())
.collect::<Vec<_>>()
.join(" "),
),
};
let (mut inner, nonce) = (
self.inner
.set_response_type(&response_type)
.add_extra_param("nonce", self.nonce.secret().clone()),
self.nonce,
);
if !self.acr_values.is_empty() {
inner = inner.add_extra_param("acr_values", join_vec(&self.acr_values));
}
if !self.claims_locales.is_empty() {
inner = inner.add_extra_param("claims_locales", join_vec(&self.claims_locales));
}
if let Some(ref display) = self.display {
inner = inner.add_extra_param("display", display.as_ref());
}
if let Some(ref id_token_hint) = self.id_token_hint {
inner = inner.add_extra_param("id_token_hint", id_token_hint);
}
if let Some(ref login_hint) = self.login_hint {
inner = inner.add_extra_param("login_hint", login_hint.secret());
}
if let Some(max_age) = self.max_age {
inner = inner.add_extra_param("max_age", max_age.as_secs().to_string());
}
if !self.prompts.is_empty() {
inner = inner.add_extra_param("prompt", join_vec(&self.prompts));
}
if !self.ui_locales.is_empty() {
inner = inner.add_extra_param("ui_locales", join_vec(&self.ui_locales));
}
let (url, state) = inner.url();
(url, state, nonce)
}
}
pub trait TokenResponse<AC, GC, JE, JS, JT, TT>: OAuth2TokenResponse<TT>
where
AC: AdditionalClaims,
GC: GenderClaim,
JE: JweContentEncryptionAlgorithm<JT>,
JS: JwsSigningAlgorithm<JT>,
JT: JsonWebKeyType,
TT: TokenType,
{
fn id_token(&self) -> Option<&IdToken<AC, GC, JE, JS, JT>>;
}
impl<AC, EF, GC, JE, JS, JT, TT> TokenResponse<AC, GC, JE, JS, JT, TT>
for StandardTokenResponse<IdTokenFields<AC, EF, GC, JE, JS, JT>, TT>
where
AC: AdditionalClaims,
EF: ExtraTokenFields,
GC: GenderClaim,
JE: JweContentEncryptionAlgorithm<JT>,
JS: JwsSigningAlgorithm<JT>,
JT: JsonWebKeyType,
TT: TokenType,
{
fn id_token(&self) -> Option<&IdToken<AC, GC, JE, JS, JT>> {
self.extra_fields().id_token()
}
}
fn join_vec<T>(entries: &[T]) -> String
where
T: AsRef<str>,
{
entries
.iter()
.map(AsRef::as_ref)
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use std::time::Duration;
use oauth2::{AuthUrl, ClientId, ClientSecret, CsrfToken, RedirectUrl, Scope, TokenUrl};
use crate::core::CoreAuthenticationFlow;
use crate::core::{CoreAuthDisplay, CoreAuthPrompt, CoreClient, CoreIdToken, CoreResponseType};
use crate::IssuerUrl;
use crate::{
AuthenticationContextClass, AuthenticationFlow, JsonWebKeySet, LanguageTag, LoginHint,
Nonce,
};
fn new_client() -> CoreClient {
color_backtrace::install();
CoreClient::new(
ClientId::new("aaa".to_string()),
Some(ClientSecret::new("bbb".to_string())),
IssuerUrl::new("https://example".to_string()).unwrap(),
AuthUrl::new("https://example/authorize".to_string()).unwrap(),
Some(TokenUrl::new("https://example/token".to_string()).unwrap()),
None,
JsonWebKeySet::default(),
)
}
#[test]
fn test_authorize_url_minimal() {
let client = new_client();
let (authorize_url, _, _) = client
.authorize_url(
AuthenticationFlow::AuthorizationCode::<CoreResponseType>,
|| CsrfToken::new("CSRF123".to_string()),
|| Nonce::new("NONCE456".to_string()),
)
.url();
assert_eq!(
"https://example/authorize?response_type=code&client_id=aaa&\
state=CSRF123&scope=openid&nonce=NONCE456",
authorize_url.to_string()
);
}
#[test]
fn test_authorize_url_implicit_with_access_token() {
let client = new_client();
let (authorize_url, _, _) = client
.authorize_url(
AuthenticationFlow::<CoreResponseType>::Implicit(true),
|| CsrfToken::new("CSRF123".to_string()),
|| Nonce::new("NONCE456".to_string()),
)
.url();
assert_eq!(
"https://example/authorize?response_type=id_token+token&client_id=aaa&\
state=CSRF123&scope=openid&nonce=NONCE456",
authorize_url.to_string()
);
}
#[test]
fn test_authorize_url_hybrid() {
let client = new_client();
let (authorize_url, _, _) = client
.authorize_url(
AuthenticationFlow::Hybrid(vec![
CoreResponseType::Code,
CoreResponseType::Extension("other".to_string()),
]),
|| CsrfToken::new("CSRF123".to_string()),
|| Nonce::new("NONCE456".to_string()),
)
.url();
assert_eq!(
"https://example/authorize?response_type=code+other&client_id=aaa&\
state=CSRF123&scope=openid&nonce=NONCE456",
authorize_url.to_string()
);
}
#[test]
fn test_authorize_url_full() {
let client = new_client()
.set_redirect_uri(RedirectUrl::new("http://localhost:8888/".to_string()).unwrap());
let flow = CoreAuthenticationFlow::AuthorizationCode;
fn new_csrf() -> CsrfToken {
CsrfToken::new("CSRF123".to_string())
}
fn new_nonce() -> Nonce {
Nonce::new("NONCE456".to_string())
}
let (authorize_url, _, _) = client
.authorize_url(flow.clone(), new_csrf, new_nonce)
.add_scope(Scope::new("email".to_string()))
.set_display(CoreAuthDisplay::Touch)
.add_prompt(CoreAuthPrompt::Login)
.add_prompt(CoreAuthPrompt::Consent)
.set_max_age(Duration::from_secs(1800))
.add_ui_locale(LanguageTag::new("fr-CA".to_string()))
.add_ui_locale(LanguageTag::new("fr".to_string()))
.add_ui_locale(LanguageTag::new("en".to_string()))
.add_auth_context_value(AuthenticationContextClass::new(
"urn:mace:incommon:iap:silver".to_string(),
))
.url();
assert_eq!(
"https://example/authorize?response_type=code&client_id=aaa&\
state=CSRF123&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email&\
nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\
max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en",
authorize_url.to_string()
);
let serialized_jwt =
"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXVkIjpbIm15X2NsaWVudCJdL\
CJleHAiOjE1NDQ5MzIxNDksImlhdCI6MTU0NDkyODU0OSwiYXV0aF90aW1lIjoxNTQ0OTI4NTQ4LCJub25jZSI\
6InRoZV9ub25jZSIsImFjciI6InRoZV9hY3IiLCJzdWIiOiJzdWJqZWN0In0.gb5HuuyDMu-LvYvG-jJNIJPEZ\
823qNwvgNjdAtW0HJpgwJWhJq0hOHUuZz6lvf8ud5xbg5GOo0Q37v3Ke08TvGu6E1USWjecZzp1aYVm9BiMvw5\
EBRUrwAaOCG2XFjuOKUVfglSMJnRnoNqVVIWpCAr1ETjZzRIbkU3n5GQRguC5CwN5n45I3dtjoKuNGc2Ni-IMl\
J2nRiCJOl2FtStdgs-doc-A9DHtO01x-5HCwytXvcE28Snur1JnqpUgmWrQ8gZMGuijKirgNnze2Dd5BsZRHZ2\
CLGIwBsCnauBrJy_NNlQg4hUcSlGsuTa0dmZY7mCf4BN2WCpyOh0wgtkAgQ";
let id_token = serde_json::from_value::<CoreIdToken>(serde_json::Value::String(
serialized_jwt.to_string(),
))
.unwrap();
let (authorize_url, _, _) = client
.authorize_url(flow, new_csrf, new_nonce)
.add_scope(Scope::new("email".to_string()))
.set_display(CoreAuthDisplay::Touch)
.set_id_token_hint(&id_token)
.set_login_hint(LoginHint::new("foo@bar.com".to_string()))
.add_prompt(CoreAuthPrompt::Login)
.add_prompt(CoreAuthPrompt::Consent)
.set_max_age(Duration::from_secs(1800))
.add_ui_locale(LanguageTag::new("fr-CA".to_string()))
.add_ui_locale(LanguageTag::new("fr".to_string()))
.add_ui_locale(LanguageTag::new("en".to_string()))
.add_auth_context_value(AuthenticationContextClass::new(
"urn:mace:incommon:iap:silver".to_string(),
))
.add_extra_param("foo", "bar")
.url();
assert_eq!(
format!(
"https://example/authorize?response_type=code&client_id=aaa&state=CSRF123&\
redirect_uri=http%3A%2F%2Flocalhost%3A8888%2F&scope=openid+email&foo=bar&\
nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\
id_token_hint={}&login_hint=foo%40bar.com&\
max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en",
serialized_jwt
),
authorize_url.to_string()
);
}
#[test]
fn test_authorize_url_redirect_url_override() {
let client = new_client()
.set_redirect_uri(RedirectUrl::new("http://localhost:8888/".to_string()).unwrap());
let flow = CoreAuthenticationFlow::AuthorizationCode;
fn new_csrf() -> CsrfToken {
CsrfToken::new("CSRF123".to_string())
}
fn new_nonce() -> Nonce {
Nonce::new("NONCE456".to_string())
}
let (authorize_url, _, _) = client
.authorize_url(flow, new_csrf, new_nonce)
.add_scope(Scope::new("email".to_string()))
.set_display(CoreAuthDisplay::Touch)
.add_prompt(CoreAuthPrompt::Login)
.add_prompt(CoreAuthPrompt::Consent)
.set_max_age(Duration::from_secs(1800))
.add_ui_locale(LanguageTag::new("fr-CA".to_string()))
.add_ui_locale(LanguageTag::new("fr".to_string()))
.add_ui_locale(LanguageTag::new("en".to_string()))
.add_auth_context_value(AuthenticationContextClass::new(
"urn:mace:incommon:iap:silver".to_string(),
))
.set_redirect_uri(Cow::Owned(
RedirectUrl::new("http://localhost:8888/alternative".to_string()).unwrap(),
))
.url();
assert_eq!(
"https://example/authorize?response_type=code&client_id=aaa&\
state=CSRF123&redirect_uri=http%3A%2F%2Flocalhost%3A8888%2Falternative&scope=openid+email&\
nonce=NONCE456&acr_values=urn%3Amace%3Aincommon%3Aiap%3Asilver&display=touch&\
max_age=1800&prompt=login+consent&ui_locales=fr-CA+fr+en",
authorize_url.to_string()
);
}
}