feat(auth): support returning extra fields from token exchange (#700)
* feat(auth): support returning extra fields that may be returned from token generation exchange_code_for_token and refresh_token now return a StandardTokenResponse which includes any additionalfields which might have been sent by the vendor BREAKING CHANGE: Return type of exchange_code_for_token and refresh_token has changed and may require code changes. * fix: doc links
This commit is contained in:
parent
9299fd3792
commit
78d959fcd4
1 changed files with 51 additions and 13 deletions
|
|
@ -7,16 +7,16 @@ use std::{
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use oauth2::{
|
use oauth2::{
|
||||||
AsyncHttpClient, AuthType, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken,
|
AsyncHttpClient, AuthType, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken,
|
||||||
EmptyExtraTokenFields, HttpClientError, HttpRequest, HttpResponse, PkceCodeChallenge,
|
EmptyExtraTokenFields, ExtraTokenFields, HttpClientError, HttpRequest, HttpResponse,
|
||||||
PkceCodeVerifier, RedirectUrl, RefreshToken, RequestTokenError, Scope, StandardTokenResponse,
|
PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, RefreshToken, RequestTokenError, Scope,
|
||||||
TokenResponse, TokenUrl,
|
StandardTokenResponse, TokenResponse, TokenUrl, basic::BasicTokenType,
|
||||||
basic::{BasicClient, BasicTokenType},
|
|
||||||
};
|
};
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
Client as HttpClient, IntoUrl, StatusCode, Url,
|
Client as HttpClient, IntoUrl, StatusCode, Url,
|
||||||
header::{AUTHORIZATION, WWW_AUTHENTICATE},
|
header::{AUTHORIZATION, WWW_AUTHENTICATE},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use tracing::{debug, error, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
@ -126,6 +126,32 @@ pub struct StoredAuthorizationState {
|
||||||
pub created_at: u64,
|
pub created_at: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A transparent wrapper around a JSON object that captures any extra fields returned by the
|
||||||
|
/// authorization server during token exchange that are not part of the standard OAuth 2.0 token
|
||||||
|
/// response.
|
||||||
|
///
|
||||||
|
/// OAuth providers may include non-standard fields alongside the
|
||||||
|
/// standard OAuth fields. Those fields are collected here so callers
|
||||||
|
/// can inspect them without losing data.
|
||||||
|
///
|
||||||
|
/// The inner [`HashMap<String, Value>`] maps field names to their raw JSON values.
|
||||||
|
///
|
||||||
|
/// # Accessing extra fields
|
||||||
|
///
|
||||||
|
/// Extra fields are available through [`StandardTokenResponse::extra_fields()`], which returns a
|
||||||
|
/// reference to this struct. Use the inner map (`.0`) to look up individual fields by name:
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// // Obtain the token response from the AuthorizationManager, then:
|
||||||
|
/// if let Some(value) = token_response.extra_fields().0.get("vendorSpecificField") {
|
||||||
|
/// println!("vendorSpecificField = {value}");
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
pub struct VendorExtraTokenFields(pub HashMap<String, Value>);
|
||||||
|
|
||||||
|
impl ExtraTokenFields for VendorExtraTokenFields {}
|
||||||
|
|
||||||
impl StoredAuthorizationState {
|
impl StoredAuthorizationState {
|
||||||
pub fn new(pkce_verifier: &PkceCodeVerifier, csrf_token: &CsrfToken) -> Self {
|
pub fn new(pkce_verifier: &PkceCodeVerifier, csrf_token: &CsrfToken) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -345,7 +371,18 @@ pub struct OAuthClientConfig {
|
||||||
|
|
||||||
// add type aliases for oauth2 types
|
// add type aliases for oauth2 types
|
||||||
type OAuthErrorResponse = oauth2::StandardErrorResponse<oauth2::basic::BasicErrorResponseType>;
|
type OAuthErrorResponse = oauth2::StandardErrorResponse<oauth2::basic::BasicErrorResponseType>;
|
||||||
pub type OAuthTokenResponse = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;
|
|
||||||
|
/// The token response returned by the authorization server after a successful OAuth 2.0 flow.
|
||||||
|
///
|
||||||
|
/// This is a [`StandardTokenResponse`] parameterised with [`VendorExtraTokenFields`], which means
|
||||||
|
/// it carries both the standard OAuth fields and
|
||||||
|
/// any vendor-specific fields the server may have included in the JSON response body.
|
||||||
|
///
|
||||||
|
/// # Accessing vendor-specific fields
|
||||||
|
///
|
||||||
|
/// Call [`extra_fields()`][OAuthTokenResponse::extra_fields] to obtain a reference to the
|
||||||
|
/// [`VendorExtraTokenFields`] wrapper, then index into its inner map.
|
||||||
|
pub type OAuthTokenResponse = StandardTokenResponse<VendorExtraTokenFields, BasicTokenType>;
|
||||||
type OAuthTokenIntrospection =
|
type OAuthTokenIntrospection =
|
||||||
oauth2::StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>;
|
oauth2::StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>;
|
||||||
type OAuthRevocableToken = oauth2::StandardRevocableToken;
|
type OAuthRevocableToken = oauth2::StandardRevocableToken;
|
||||||
|
|
@ -581,7 +618,7 @@ impl AuthorizationManager {
|
||||||
let redirect_url = RedirectUrl::new(config.redirect_uri.clone())
|
let redirect_url = RedirectUrl::new(config.redirect_uri.clone())
|
||||||
.map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?;
|
.map_err(|e| AuthError::OAuthError(format!("Invalid re URL: {}", e)))?;
|
||||||
|
|
||||||
let mut client_builder = BasicClient::new(client_id.clone())
|
let mut client_builder: OAuthClient = oauth2::Client::new(client_id.clone())
|
||||||
.set_auth_uri(auth_url)
|
.set_auth_uri(auth_url)
|
||||||
.set_token_uri(token_url)
|
.set_token_uri(token_url)
|
||||||
.set_redirect_uri(redirect_url);
|
.set_redirect_uri(redirect_url);
|
||||||
|
|
@ -882,7 +919,7 @@ impl AuthorizationManager {
|
||||||
&self,
|
&self,
|
||||||
code: &str,
|
code: &str,
|
||||||
csrf_token: &str,
|
csrf_token: &str,
|
||||||
) -> Result<StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>, AuthError> {
|
) -> Result<OAuthTokenResponse, AuthError> {
|
||||||
debug!("start exchange code for token: {:?}", code);
|
debug!("start exchange code for token: {:?}", code);
|
||||||
let oauth_client = self
|
let oauth_client = self
|
||||||
.oauth_client
|
.oauth_client
|
||||||
|
|
@ -1017,9 +1054,7 @@ impl AuthorizationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// refresh access token
|
/// refresh access token
|
||||||
pub async fn refresh_token(
|
pub async fn refresh_token(&self) -> Result<OAuthTokenResponse, AuthError> {
|
||||||
&self,
|
|
||||||
) -> Result<StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>, AuthError> {
|
|
||||||
let oauth_client = self
|
let oauth_client = self
|
||||||
.oauth_client
|
.oauth_client
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -1551,7 +1586,7 @@ impl AuthorizationSession {
|
||||||
&self,
|
&self,
|
||||||
code: &str,
|
code: &str,
|
||||||
csrf_token: &str,
|
csrf_token: &str,
|
||||||
) -> Result<StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>, AuthError> {
|
) -> Result<OAuthTokenResponse, AuthError> {
|
||||||
self.auth_manager
|
self.auth_manager
|
||||||
.exchange_code_for_token(code, csrf_token)
|
.exchange_code_for_token(code, csrf_token)
|
||||||
.await
|
.await
|
||||||
|
|
@ -1876,6 +1911,7 @@ mod tests {
|
||||||
AuthError, AuthorizationManager, AuthorizationMetadata, InMemoryStateStore,
|
AuthError, AuthorizationManager, AuthorizationMetadata, InMemoryStateStore,
|
||||||
OAuthClientConfig, ScopeUpgradeConfig, StateStore, StoredAuthorizationState, is_https_url,
|
OAuthClientConfig, ScopeUpgradeConfig, StateStore, StoredAuthorizationState, is_https_url,
|
||||||
};
|
};
|
||||||
|
use crate::transport::auth::VendorExtraTokenFields;
|
||||||
|
|
||||||
// -- url helpers --
|
// -- url helpers --
|
||||||
|
|
||||||
|
|
@ -2686,11 +2722,13 @@ mod tests {
|
||||||
use super::{OAuthTokenResponse, StoredCredentials};
|
use super::{OAuthTokenResponse, StoredCredentials};
|
||||||
|
|
||||||
fn make_token_response(access_token: &str, expires_in_secs: Option<u64>) -> OAuthTokenResponse {
|
fn make_token_response(access_token: &str, expires_in_secs: Option<u64>) -> OAuthTokenResponse {
|
||||||
use oauth2::{AccessToken, EmptyExtraTokenFields, basic::BasicTokenType};
|
use oauth2::{AccessToken, basic::BasicTokenType};
|
||||||
let mut resp = OAuthTokenResponse::new(
|
let mut resp = OAuthTokenResponse::new(
|
||||||
AccessToken::new(access_token.to_string()),
|
AccessToken::new(access_token.to_string()),
|
||||||
BasicTokenType::Bearer,
|
BasicTokenType::Bearer,
|
||||||
EmptyExtraTokenFields {},
|
VendorExtraTokenFields {
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if let Some(secs) = expires_in_secs {
|
if let Some(secs) = expires_in_secs {
|
||||||
resp.set_expires_in(Some(&std::time::Duration::from_secs(secs)));
|
resp.set_expires_in(Some(&std::time::Duration::from_secs(secs)));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue