Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ path = "examples/hyper/main.rs"
[[example]]
name = "id_token"
path = "examples/id_token.rs"

[[example]]
name = "easy_id_token"
path = "examples/easy/id_token.rs"
205 changes: 205 additions & 0 deletions examples/easy/id_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//! example of getting IDToken and print it.
//!
//! # Setup
//! 1. Create OAuth2.0 Client at google-cloud-console's credentials page
//! 2. Get your AUTH_ENDPOINT, CLIENT_ID, CLIENT_SECRET, TOKEN_ENDPOINT, REDIRECT_URI
//! 3. Set step 2's value as a static value down below
//! 4. Run with the following
//! ```not_rust
//! cargo run --example easy_id_token
//! ```
//! 5. Access the endpoint that has a login handler in your browser(default: http://localhost)
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};

use axum::{
Router,
extract::{Request, State},
response::{IntoResponse, Redirect, Response},
routing::get,
};
use axum_extra::extract::CookieJar;
use cookie::{
Cookie,
time::{Duration, OffsetDateTime},
};
use http::StatusCode;
use tiny_google_oidc::{
code::{AccessType, AdditionalScope},
config::ConfigBuilder,
easy::{create_id_token_request, generate_auth_redirect},
id_token::{IDToken, send_id_token_req},
};
use uuid::Uuid;

// Fix this value CLIENT_ID, CLIENT_SECRET, REDIRECT_URI
static AUTH_ENDPOINT: &str = "https://accounts.google.com/o/oauth2/auth";
static CLIENT_ID: &str = "my_client_id";
static CLIENT_SECRET: &str = "my_client_secret";
static TOKEN_ENDPOINT: &str = "https://oauth2.googleapis.com/token";
static REDIRECT_URI: &str = "http://localhost/auth/callback";

static COOKIE_KEY: &str = "token";

#[tokio::main]
async fn main() {
// Construct Config by using your CLIENT_ID, SECRETS, etc...
let oidc_cfg = ConfigBuilder::new()
.auth_endpoint(AUTH_ENDPOINT)
.client_id(CLIENT_ID)
.client_secret(CLIENT_SECRET)
.token_endpoint(TOKEN_ENDPOINT)
.redirect_uri(REDIRECT_URI)
.build();

// Fix port number you set at google cloud console
let listener = tokio::net::TcpListener::bind("0.0.0.0:80").await.unwrap();

let app_state = Arc::new(AppState::new(oidc_cfg));

let app = Router::new()
.route("/", get(login))
// If callback URI that you set is different, please fix
.route("/auth/callback", get(callback))
.with_state(app_state);

axum::serve(listener, app).await.unwrap();
}

#[derive(Debug)]
struct AppState {
state: Mutex<HashMap<String, String>>,
oidc_cfg: Arc<tiny_google_oidc::config::Config>,
}

impl AppState {
fn new(oidc_cfg: tiny_google_oidc::config::Config) -> Self {
AppState {
state: Mutex::default(),
oidc_cfg: Arc::new(oidc_cfg),
}
}

fn insert(&self, store_key: &str, csrf_token: &str) {
let mut guard = self.state.lock().unwrap();
guard.insert(store_key.to_string(), csrf_token.to_string());
}

fn get(&self, store_key: &str) -> Option<String> {
let guard = self.state.lock().unwrap();
guard.get(store_key).map(|v| v.to_string())
}
}

async fn login(
State(app_state): State<Arc<AppState>>,
jar: CookieJar,
) -> Result<impl IntoResponse, AppError> {
let (csrf_token, _nonce, redirect_uri) = generate_auth_redirect(
&app_state.oidc_cfg,
AccessType::Online,
AdditionalScope::Both,
)
.map_err(WrapOIDCError)?;

let csrf_token_store_key = Uuid::new_v4();
app_state.insert(&csrf_token_store_key.to_string(), csrf_token.value());

// Create cookie that is holding a key which indicate csrf_token_value
let mut cookie = Cookie::new(COOKIE_KEY, csrf_token_store_key.to_string());
cookie.set_http_only(true);
set_cookie_expires(&mut cookie, Duration::minutes(5));

// Set-Cookie ant Redirect as a response
Ok((jar.add(cookie), Redirect::to(&redirect_uri)))
}

fn set_cookie_expires(cookie: &mut Cookie, delta: Duration) {
let time = OffsetDateTime::now_utc() + delta;
cookie.set_expires(time);
}

async fn callback(
State(app_state): State<Arc<AppState>>,
jar: CookieJar,
req: Request,
) -> Result<impl IntoResponse, AppError> {
let csrf_token_key = get_csrf_token_key_from_cookie(&jar)?;
// fetch csrf_token_val by using key that you got from cookie
let csrf_token_val = app_state
.get(&csrf_token_key)
.ok_or(AppError::GenURL)?
.to_owned();

let id_token_req = create_id_token_request(&app_state.oidc_cfg, &csrf_token_val, req)
.map_err(WrapOIDCError)?;

// Send request to google
// This is effect
let id_token_res = send_id_token_req(&id_token_req)
.await
.map_err(WrapOIDCError)?;

// print IDTokenResponse
// It has some value, such as refresh_token, access_token, etc...
println!("----IDTokenResponse----");
println!("{id_token_res:#?}");

// encode IDToken from raw string
let id_token = IDToken::from_id_token_raw(id_token_res.id_token()).map_err(WrapOIDCError)?;

// print id_token
println!("----IDToken----");
println!("{id_token:#?}");

Ok((StatusCode::OK, "login success"))
}

fn get_csrf_token_key_from_cookie(jar: &CookieJar) -> Result<String, AppError> {
let cookie = jar.get(COOKIE_KEY).ok_or(AppError::CookieNotFound)?;
Ok(cookie.value_trimmed().to_string())
}

#[derive(Debug, Clone)]
enum AppError {
CookieNotFound,
GenURL,
CSRFNotMatch(String),
SendStatus((StatusCode, String)),
Others(tiny_google_oidc::error::Error),
}

impl IntoResponse for AppError {
fn into_response(self) -> Response {
match self {
AppError::CookieNotFound => {
{ (StatusCode::BAD_REQUEST, "cookie not found to auth").into_response() }
.into_response()
}
AppError::GenURL => {
(StatusCode::INTERNAL_SERVER_ERROR, "failed to generate url").into_response()
}
AppError::Others(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
}
AppError::CSRFNotMatch(e) => (StatusCode::BAD_REQUEST, e.to_string()).into_response(),
AppError::SendStatus((status, msg)) => (status, msg).into_response(),
}
}
}

#[derive(Debug, Clone)]
struct WrapOIDCError(tiny_google_oidc::error::Error);

use tiny_google_oidc::error::Error;
impl From<WrapOIDCError> for AppError {
fn from(value: WrapOIDCError) -> Self {
match value.0 {
Error::CSRFNotMatch => AppError::CSRFNotMatch(value.0.to_string()),
Error::SendStatus(status) => AppError::SendStatus((status, value.0.to_string())),
_ => AppError::Others(value.0),
}
}
}
110 changes: 110 additions & 0 deletions src/easy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! A module to simplify implementing the OIDC authentication flow.
//!
//! This module provides easy-to-use helper functions that combine the public API to reduce boilerplate.
//! It generates a CSRF token and a nonce internally, so these functions have some side effects.
//!
//! # Examples
//! See [example/easy/id_token](https://github.com/nakaryo716/tiny_google_oidc/examples/easy/id_token.rs) for usage examples.
use crate::{
code::{AccessType, AdditionalScope, Code, CodeRequest, QueryExtractor, RawCodeResponse},
config::Config,
csrf_token::CSRFToken,
error::Error,
id_token::IDTokenRequest,
nonce::Nonce,
};

/// Generates a redirect URI for obtaining an OIDC authorization code .
///
/// Internally, this function generates a new [`CSRFToken`] and [`Nonce`],
/// then constructs a redirect URI according to the specified arguments.
/// You should store the CSRF token and nonce as needed for later validation.
///
/// # Arguments
/// ## config
/// The OIDC provider configuration, contains Client ID, Secrets, etc...
/// ## access_type
/// If you want to obtain refresh token, set [`AccessType::Offline`].
/// See [google document](https://developers.google.com/identity/openid-connect/openid-connect#refresh-tokens)
/// ## scope
/// Additional scopes to request.
/// - If the [`AdditionalScope::Profile`] scope value is present, the ID token might (but is not guaranteed to)
/// include the user's default profile claims.
/// - If the [`AdditionalScope::Email`] scope value is present, the ID token includes email and email_verified claims.
/// - If the [`AdditionalScope::Both`] scope value is present, the ID token includes
/// the user's default profile, email and email_verified claims.
/// - If the [`AdditionalScope::None`] scope value is present, the ID token dose not include any additional claims.
///
/// See [google documentation](https://developers.google.com/identity/openid-connect/openid-connect#scope-param)
///
/// # Returns
/// A tuple containing:
/// - 0: The generated CSRF token.
/// - 1: The generated nonce.
/// - 2: The redirect URI as a [`String`].
///
/// # Errors
/// Returns an error if token generation or URI construction fails.
///
/// # Examples
/// ```no_run
/// let (csrf_token, nonce, uri) = generate_auth_redirect(
/// &config,
/// AccessType::Offline,
/// AdditionalScope::Both
/// )?;
/// ```
pub fn generate_auth_redirect(
config: &Config,
access_type: AccessType,
scope: AdditionalScope,
) -> Result<(CSRFToken, Nonce, String), Error> {
let csrf_token = CSRFToken::new()?;
let nonce = Nonce::new();
let code_req = CodeRequest::new(access_type, config, scope, &csrf_token, &nonce);
let uri = code_req.try_into_url()?.to_string();
Ok((csrf_token, nonce, uri))
}

/// Creates an [`IDTokenRequest`] after validating the CSRF token.
///
/// This function validates that the provided CSRF token string (usually stored on the server)
/// matches the one returned from the Google authentication response (given as `query_src`).
/// If the token matches, it constructs an [`IDTokenRequest`]; otherwise, it returns an error.
///
/// # Arguments
/// ## `config`
/// The OIDC provider configuration.
/// ## `csrf_token_str`
/// The CSRF token string stored on the server.
/// ## `query_src`
/// A type that is implementing the [`QueryExtractor`] trait.
/// The trait method returns a URL-encoded query string that is returned from Google.
///
/// # Returns
/// An [`IDTokenRequest`] if CSRF validation succeeds.
///
/// # Errors
/// Returns an error if CSRF validation fails or if the query cannot be parsed.
///
/// # Examples
/// ```no_run
/// fn callback_handler(config: &Config, stored_csrf_token: &str, req: Request) -> Result<(), Error> {
/// // http::Request is implemented QueryExtractor trait
/// let id_token_req = create_id_token_request(
/// config,
/// stored_csrf_token,
/// req
/// )?;
/// }
/// ```
pub fn create_id_token_request<'a, Q: QueryExtractor>(
config: &'a Config,
csrf_token_str: &'a str,
query_src: Q,
) -> Result<IDTokenRequest<'a>, Error> {
let raw_code = RawCodeResponse::new(query_src)?;
let code = Code::new_with_verify_csrf(raw_code, csrf_token_str)?;
let id_token_req = IDTokenRequest::new(config, code);
Ok(id_token_req)
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@
pub mod code;
pub mod config;
pub mod csrf_token;
pub mod easy;
pub mod error;
pub mod id_token;
pub mod nonce;
pub mod refresh_token;
pub mod revoke_token;

pub use easy::{create_id_token_request, generate_auth_redirect};
Loading