3 个不稳定版本

0.2.0 2024年6月25日
0.1.1 2024年3月21日
0.1.0 2024年3月20日

#1381 in 网页编程

MIT 许可证

105KB
1K SLoC

Loco OAuth2

Loco OAuth2 是一个用于 Loco API 的简单 OAuth2 初始化器。它设计成一个小巧且易于使用的库,用于在您的应用程序中实现 OAuth2。

文档

官方的 RFC 6749 OAuth2 文档可以在这里找到。
Shuttle 教程可以在这里找到。

OAuth2 是什么?

OAuth2 是一种协议,允许用户授予第三方网站或应用程序访问其受保护资源的权限,而无需透露其长期凭证或身份。为此,用户需要通过第三方网站进行身份验证并授权客户端应用程序访问。

授权类型

OAuth2 中有多种授权类型。目前 Loco OAuth2 支持的授权类型有 Authorization Code Grant。客户端凭证授权、隐式授权等将在未来的版本中提供。

目录

  1. 安装
  2. 术语表
  3. 配置(授权代码授权)
  4. 初始化
  5. 迁移
  6. 模型
  7. 控制器

安装

Cargo

cargo add loco-oauth2

Cargo.toml

[dependencies]
loco-oauth2 = "0.2.0"

术语表

OAuth2ClientGrantEnum OAuth2ClientGrantEnum 是不同 OAuth2 授权类型的枚举,OAuth2 客户端将属于 OAuth2ClientGrantEnum 之一。
OAuth2ClientStore 用于管理一个或多个 OAuth2 客户端的抽象实现。
authorization_code::客户端 使用授权代码授权的客户端。

配置(授权代码授权)

secret_key 用于加密私有 cookie jar。它必须超过 64 字节。如果没有提供,它将被自动生成。以下是如何生成私有 cookie 密钥的示例。

# Cargo.toml
rand = "0.9.0-alpha.1"
axum-extra = { version = "0.9.3", features = ["cookie-private"]}
// src/main.rs
use axum_extra::extract::cookie::Key;
use rand::{Rng, thread_rng};

fn main() {
    // Generate a cryptographically random key of 64 bytes
    let mut rng = thread_rng();
    let mut random_key = [0u8; 64];
    rng.fill(&mut random_key);
    match Key::try_from(&random_key[..]) {
        Ok(key) => {
            println!("Random key: {:?}", key.master());
        }
        Err(e) => {
            println!("Error: {:?}", e);
        }
    }
}

OAuth2 配置

OAuth2 配置在 config/*.yaml 文件中完成。使用 oauth2 部分配置 OAuth2 客户端。

本示例使用Google Cloud作为OAuth2提供者。您需要一个Google Cloud项目,并使用OAuth客户端ID选项创建用于client_idclient_secret的OAuth2凭证。redirect_url是提供者的服务器回调端点,应在创建OAuth2客户端ID时设置在Authorised redirect URIs部分。

# config/*.yaml
# Initializers Configuration
initializers:
  oauth2:
    secret_key: {{get_env(name="OAUTH_PRIVATE_KEY", default="144, 76, 183, 1, 15, 184, 233, 174, 214, 251, 190, 186, 122, 61, 74, 84, 225, 110, 189, 115, 10, 251, 133, 128, 52, 46, 15, 66, 85, 1, 245, 73, 27, 113, 189, 15, 209, 205, 61, 100, 73, 31, 18, 58, 235, 105, 141, 36, 70, 92, 231, 151, 27, 32, 243, 117, 30, 244, 110, 89, 233, 196, 137, 130")}} # Optional, key for Private Cookie Jar, must be more than 64 bytes
    authorization_code: # Authorization code grant type
      - client_identifier: google # Identifier for the OAuth2 provider. Replace 'google' with your provider's name if different, must be unique within the oauth2 config.
        client_credentials:
          client_id: {{get_env(name="OAUTH_CLIENT_ID", default="oauth_client_id")}} # Replace with your OAuth2 client ID.
          client_secret: {{get_env(name="OAUTH_CLIENT_SECRET", default="oauth_client_secret")}} # Replace with your OAuth2 client secret.
        url_config:
          auth_url: {{get_env(name="AUTH_URL", default="https://127.0.0.1/o/oauth2/auth")}} # authorization endpoint from the provider
          token_url: {{get_env(name="TOKEN_URL", default="https://www.googleapis.com/oauth2/v3/token")}} # token endpoint from the provider for exchanging the authorization code for an access token
          redirect_url: {{get_env(name="REDIRECT_URL", default="https://127.0.0.1:3000/api/oauth2/google/callback/cookie")}} # server callback endpoint for the provider, for default jwt route use 'default="https://127.0.0.1:3000/api/oauth2/google/callback/cookie"'
          profile_url: {{get_env(name="PROFILE_URL", default="https://openidconnect.googleapis.com/v1/userinfo")}} # user profile endpoint from the provider for getting user data
          scopes:
            - {{get_env(name="SCOPES_1", default="https://www.googleapis.com/auth/userinfo.email")}} # Scopes for requesting access to user data
            - {{get_env(name="SCOPES_2", default="https://www.googleapis.com/auth/userinfo.profile")}} # Scopes for requesting access to user data
        cookie_config:
          protected_url: {{get_env(name="PROTECTED_URL", default="https://127.0.0.1:3000/api/oauth2/protected")}} # Optional for jwt - For redirecting to protect url in cookie to prevent XSS attack
        timeout_seconds: 600 # Optional, default 600 seconds

初始化

我们将使用Loco框架的初始化功能来初始化OAuth2客户端。

首先,我们需要创建一个用于存储csrf token的会话存储。我们将使用AxumSessionStore来完成此任务。我们将为AxumSessionStore创建一个新的初始化器结构体并实现Initializer特质。

# Cargo.toml
# axum sessions
axum_session = { version = "0.14.0" }
// src/initializers/axum_session.rs
use async_trait::async_trait;
use axum::Router as AxumRouter;
use loco_rs::prelude::*;

pub struct AxumSessionInitializer;

#[async_trait]
impl Initializer for AxumSessionInitializer {
    fn name(&self) -> String {
        "axum-session".to_string()
    }

    async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result<AxumRouter> {
        // Create the session store configuration
        let session_config =
            axum_session::SessionConfig::default().with_table_name("sessions_table");
        // Create the session store
        let session_store =
            axum_session::SessionStore::<axum_session::SessionNullPool>::new(None, session_config)
                .await
                .unwrap();
        // Add the session store to the AxumRouter as an extension
        let router = router.layer(axum_session::SessionLayer::new(session_store));
        Ok(router)
    }
}

我们将为OAuth2ClientStore创建一个新的初始化器结构体并实现Initializer特质。在after_routes函数中,我们将从config获取oauth2设置并创建OAuth2ClientStore,并将其作为扩展添加到AxumRouter

// src/initializers/oauth2.rs
use axum::{async_trait, Extension, Router as AxumRouter};
use loco_oauth2::{config::Config, OAuth2ClientStore};
use loco_rs::prelude::*;

pub struct OAuth2StoreInitializer;

#[async_trait]
impl Initializer for OAuth2StoreInitializer {
    fn name(&self) -> String {
        "oauth2-store".to_string()
    }
    async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result<AxumRouter> {
        // Get all the settings from the config
        let settings = ctx.config.initializers.clone().ok_or_else(|| {
            Error::Message("Initializers config not configured for OAuth2".to_string())
        })?;
        // Get the oauth2 config in json format
        let oauth2_config_value = settings
            .get("oauth2")
            .ok_or(Error::Message(
                "Oauth2 config not found in Initializer configuration".to_string(),
            ))?
            .clone();
        // Convert the oauth2 config json to OAuth2Config
        let oauth2_config: Config = oauth2_config_value.try_into().map_err(|e| {
            tracing::error!(error = ?e, "could not convert oauth2 config from yaml");
            Error::Message("could not convert oauth2 config from yaml".to_string())
        })?;
        // Create the OAuth2ClientStore
        let oauth2_store = OAuth2ClientStore::new(oauth2_config).map_err(|e| {
            tracing::error!(error = ?e, "could not create oauth2 store from config");
            Error::Message("could not create oauth2 store from config".to_string())
        })?;
        // Add the OAuth2ClientStore to the AxumRouter as an extension
        Ok(router.layer(Extension(oauth2_store)))
    }
}

不要忘记将初始化器添加到App结构体。

// src/app.rs
pub struct App;

#[async_trait]
impl Hooks for App {
    async fn initializers(_ctx: &AppContext) -> Result<Vec<Box<dyn Initializer>>> {
        Ok(vec![
            Box::new(initializers::axum_session::AxumSessionInitializer),
            Box::new(initializers::oauth2::OAuth2StoreInitializer),
        ])
    }
}

迁移

安装

需要在迁移文件夹中安装loco-oauth2库。

# Within migration folder
cargo add loco-oauth2

迁移脚本

需要迁移以创建用于OAuth2ClientStoreo_auth2_sessions表。

o_auth2_sessions表用于将用户连接到OAuth2会话,并用于存储会话数据。

您可以从loco_oauth2导入迁移脚本到您的迁移文件夹,而不是手动创建表。

// migration/src/lib.rs
use loco_oauth2::migration;
pub use sea_orm_migration::prelude::*;

pub struct Migrator;

#[async_trait::async_trait]
impl MigratorTrait for Migrator {
    fn migrations() -> Vec<Box<dyn MigrationTrait>> {
        vec![
            Box::new(m20220101_000001_users::Migration),
            Box::new(m20231103_114510_notes::Migration),
            // Register OAuth2 sessions migration here
            Box::new(migration::m20240101_000000_oauth2_sessions::Migration),
        ]
    }
}

以下是o_auth2_sessions表的结构。

CREATE TABLE o_auth2_sessions
(
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
    id         SERIAL PRIMARY KEY,
    session_id VARCHAR                             NOT NULL,
    expires_at TIMESTAMP                           NOT NULL,
    user_id    INTEGER                             NOT NULL
        CONSTRAINT "fk-sessions-users"
            REFERENCES users
            ON UPDATE CASCADE
            ON DELETE CASCADE
);

使用以下命令运行迁移。

cargo loco db migrate

然后使用以下命令生成所有模型。

cargo loco db entities

模型

从OAuth2提供者返回的用户详细信息形状取决于您的设置范围。以下是Google OAuth2提供者的用户模型示例。您可以根据所需内容更改字段。有关作用域的更多信息,请参阅Google作用域

// src/models/user.rs
/// `OAuth2UserProfile` user profile information via scopes
/// https://developers.google.com/identity/openid-connect/openid-connect#obtainuserinfo
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct OAuth2UserProfile {
    // https://www.googleapis.com/auth/userinfo.email	See your primary Google Account email address
    pub email: String,
    // https://www.googleapis.com/auth/userinfo.profile   See your personal info, including any personal info you've made publicly available
    pub name: String,
    // sub field is unique
    pub sub: String,
    pub email_verified: bool,
    pub given_name: Option<String>, // Some accounts don't have this field
    pub family_name: Option<String>, // Some accounts don't have this field
    pub picture: Option<String>, // Some accounts don't have this field
    pub locale: Option<String>, // Some accounts don't have this field
}

接下来,我们需要为users::Model模型和o_auth2_sessions::Model实现2个特质。

OAuth2UserTrait 示例

// src/models/users.rs
use loco_oauth2::models::users::OAuth2UserTrait;
use loco_rs::{auth::jwt, hash, prelude::*};
use super::o_auth2_sessions;
use async_trait::async_trait;
use chrono::offset::Local;

#[async_trait]
impl OAuth2UserTrait<OAuth2UserProfile> for Model {
    /// Asynchronously finds user by OAuth2 session id.
    /// # Arguments
    /// * `db` - Database connection
    /// * `cookie` - OAuth2 session id
    ///
    /// # Returns
    /// * `Self` - The `OAuth2UserTrait` struct
    ///
    /// # Errors
    /// * `ModelError` - When could not find the user in the DB
    async fn find_by_oauth2_session_id(
        db: &DatabaseConnection,
        session_id: &str,
    ) -> ModelResult<Self> {
        // find the session by the session id
        let session = o_auth2_sessions::Entity::find()
            .filter(super::_entities::o_auth2_sessions::Column::SessionId.eq(session_id))
            .one(db)
            .await?
            .ok_or_else(|| ModelError::EntityNotFound)?;
        // if the session is found, find the user by the user id
        let user = users::Entity::find()
            .filter(users::Column::Id.eq(session.user_id))
            .one(db)
            .await?;
        user.ok_or_else(|| ModelError::EntityNotFound)
    }
    /// Asynchronously upsert user with OAuth data and saves it to the
    /// database.
    /// # Arguments
    /// * `db` - Database connection
    /// * `profile` - OAuth profile
    ///
    /// # Returns
    /// * `Self` - The `OAuth2UserTrait` struct
    ///
    /// # Errors
    ///
    /// When could not save the user into the DB
    async fn upsert_with_oauth(
        db: &DatabaseConnection,
        profile: &OAuth2UserProfile,
    ) -> ModelResult<Self> {
        let txn = db.begin().await?;
        let user = match users::Entity::find()
            .filter(users::Column::Email.eq(&profile.email))
            .one(&txn)
            .await?
        {
            None => {
                // We use the sub field as the user fake password since sub is unique
                let password_hash =
                    hash::hash_password(&profile.sub).map_err(|e| ModelError::Any(e.into()))?;
                // Create the user into the database
                users::ActiveModel {
                    email: ActiveValue::set(profile.email.to_string()),
                    name: ActiveValue::set(profile.name.to_string()),
                    email_verified_at: ActiveValue::set(Some(Local::now().naive_local())),
                    password: ActiveValue::set(password_hash),
                    ..Default::default()
                }
                    .insert(&txn)
                    .await
                    .map_err(|e| {
                        tracing::error!("Error while trying to create user: {e}");
                        ModelError::Any(e.into())
                    })?
            }
            // Do nothing if user exists
            Some(user) => user,
        };
        txn.commit().await?;
        Ok(user)
    }

    /// Generates a JWT
    /// # Arguments
    /// * `secret` - JWT secret
    /// * `expiration` - JWT expiration time
    ///
    /// # Returns
    /// * `String` - JWT token
    ///
    /// # Errors
    /// * `ModelError` - When could not generate the JWT
    fn generate_jwt(&self, secret: &str, expiration: &u64) -> ModelResult<String> {
        self.generate_jwt(secret, expiration)
    }
}


OAuth2SessionsTrait 示例

// src/models/o_auth2_sessions.rs
pub use super::_entities::o_auth2_sessions::{self, ActiveModel, Entity, Model};
use super::users;
use async_trait::async_trait;
use chrono::Local;
use loco_oauth2::{
    base_oauth2::basic::BasicTokenResponse, base_oauth2::TokenResponse,
    models::oauth2_sessions::OAuth2SessionsTrait,
};
use loco_rs::prelude::*;
use sea_orm::entity::prelude::*;

#[async_trait]
impl OAuth2SessionsTrait<users::Model> for Model {
    /// Check if a session is expired from the database
    ///
    /// # Arguments
    /// db: &`DatabaseConnection` - Database connection
    /// session_id: &str - Session id
    /// # Returns
    /// A boolean
    /// # Errors
    /// Returns a `ModelError` if the session is not found
    async fn is_expired(db: &DatabaseConnection, session_id: &str) -> ModelResult<bool> {
        let oauth2_session = o_auth2_sessions::Entity::find()
            .filter(o_auth2_sessions::Column::SessionId.eq(session_id))
            .one(db)
            .await?
            .ok_or_else(|| ModelError::EntityNotFound)?;
        Ok(oauth2_session.expires_at < Local::now().naive_local())
    }

    /// Upsert a session with OAuth
    ///
    /// # Arguments
    /// db: &`DatabaseConnection` - Database connection
    /// token: &`BasicTokenResponse` - OAuth token
    /// user: &`users::Model` - User
    /// # Returns
    /// A session
    /// # Errors
    /// Returns a `ModelError` if the session cannot be upserted
    async fn upsert_with_oauth2(
        db: &DatabaseConnection,
        token: &BasicTokenResponse,
        user: &users::Model,
    ) -> ModelResult<Self> {
        let txn = db.begin().await?;
        let oauth2_session_id = token.access_token().secret().clone();
        let oauth2_session = match o_auth2_sessions::Entity::find()
            .filter(o_auth2_sessions::Column::UserId.eq(user.id))
            .one(&txn)
            .await?
        {
            Some(oauth2_session) => {
                // Update the session
                let mut oauth2_session: o_auth2_sessions::ActiveModel = oauth2_session.into();
                oauth2_session.session_id = ActiveValue::set(oauth2_session_id);
                oauth2_session.expires_at =
                    ActiveValue::set(Local::now().naive_local() + token.expires_in().unwrap());
                oauth2_session.updated_at = ActiveValue::set(Local::now().naive_local());
                oauth2_session.update(&txn).await?
            }
            None => {
                // Create the session
                o_auth2_sessions::ActiveModel {
                    session_id: ActiveValue::set(oauth2_session_id),
                    expires_at: ActiveValue::set(
                        Local::now().naive_local() + token.expires_in().unwrap(),
                    ),
                    user_id: ActiveValue::set(user.id),
                    ..Default::default()
                }
                    .insert(&txn)
                    .await?
            }
        };
        txn.commit().await?;
        Ok(oauth2_session)
    }
}

控制器

我们需要为OAuth2流程实现3个控制器。

authorization_url - 此控制器用于获取用于将用户重定向到OAuth2提供者的授权URL。

callback - 此控制器用于处理来自OAuth2提供者的回调。我们可以返回一个PrivateCookieJar(它重定向到protected路由)或一个JWT令牌。

protected - 此控制器用于保护路由免受未经授权的访问。

OAuth2Controller 示例

// src/controllers/oauth2.rs
use axum_session::SessionNullPool;
use loco_oauth2::controllers::oauth2::get_authorization_url;
use loco_oauth2::OAuth2ClientStore;
use crate::models::{o_auth2_sessions, users, users::OAuth2UserProfile};

/// The authorization URL for the `OAuth2` flow
/// This will redirect the user to the `OAuth2` provider's login page
/// and then to the callback URL

/// # Arguments
/// * `session` - The axum session
/// * `oauth_store` - The `OAuth2ClientStore` extension
/// # Returns
/// The HTML response with the link to the `OAuth2` provider's login page
/// # Errors
/// `loco_rs::errors::Error` - When the `OAuth2` client cannot be retrieved
pub async fn google_authorization_url(
    session: Session<SessionNullPool>,
    Extension(oauth2_store): Extension<OAuth2ClientStore>,
) -> Result<String> {
    // Get the `google` Authorization Code Grant client from the `OAuth2ClientStore`
    let mut client = oauth2_store
        .get_authorization_code_client("google")
        .await
        .map_err(|e| {
            tracing::error!("Error getting client: {:?}", e);
            Error::InternalServerError
        })?;
    // Get the authorization URL and save the csrf token in the session
    let auth_url = get_authorization_url(session, &mut client).await;
    drop(client);
    Ok(auth_url)
}
use axum_session::SessionNullPool;
use loco_oauth2::controllers::oauth2::callback;
use loco_oauth2::OAuth2ClientStore;
use crate::models::{o_auth2_sessions, users, users::OAuth2UserProfile};

// src/controllers/oauth2.rs
/// The callback URL for the `OAuth2` flow
/// This will exchange the code for a token and then get the user profile
/// then upsert the user and the session and set the token in a short live
/// cookie Lastly, it will redirect the user to the protected URL
/// # Arguments
/// * `ctx` - The application context
/// * `session` - The axum session
/// * `params` - The query parameters
/// * `jar` - The oauth2 private cookie jar
/// * `oauth_store` - The `OAuth2ClientStore` extension
/// # Returns
/// The response with the short live cookie and the redirect to the protected
/// URL
/// # Errors
/// * `loco_rs::errors::Error`
pub async fn google_callback_cookie(
    State(ctx): State<AppContext>,
    session: Session<SessionNullPool>,
    Query(params): Query<AuthParams>,
    // Extract the private cookie jar from the request
    jar: OAuth2PrivateCookieJar,
    Extension(oauth2_store): Extension<OAuth2ClientStore>,
) -> Result<impl IntoResponse> {
    let mut client = oauth2_store
        .get_authorization_code_client("google")
        .await
        .map_err(|e| {
            tracing::error!("Error getting client: {:?}", e);
            Error::InternalServerError
        })?;
    // This function will validate the state from the callback. Then it will exchange the code for a token and then get the user profile. After that, the function will upsert the user and the session and set the token in a short live cookie and save the cookie in the private cookie jar. Lastly, the function will create a response with the short live cookie and the redirect to the protected URL
    let response = callback::<OAuth2UserProfile, users::Model, o_auth2_sessions::Model, SessionNullPool, >(ctx, session, params, jar, &mut client).await?;
    drop(client);
    Ok(response)
}

CallbackController JWT 示例 - SPA应用

/// The callback URL for the `OAuth2` flow
/// This will exchange the code for a token and then get the user profile
/// then upsert the user and the session and set the token in a short live
/// cookie Lastly, it will redirect the user to the protected URL
/// # Generics
/// * `T` - The user profile, should implement `DeserializeOwned` and `Send`
/// * `U` - The user model, should implement `OAuth2UserTrait` and `ModelTrait`
/// * `V` - The session model, should implement `OAuth2SessionsTrait` and `ModelTrait`
/// * `W` - The database pool
/// # Arguments
/// * `ctx` - The application context
/// * `session` - The axum session
/// * `params` - The query parameters
/// * `oauth2_store` - The `OAuth2ClientStore` extension
/// # Return
/// * `Result<impl IntoResponse>` - The response with the jwt token
/// # Errors
/// * `loco_rs::errors::Error`
pub async fn google_callback_jwt(
    State(ctx): State<AppContext>,
    session: Session<SessionNullPool>,
    Query(params): Query<AuthParams>,
    Extension(oauth2_store): Extension<OAuth2ClientStore>,
) -> Result<impl IntoResponse> {
    let mut client = oauth2_store
        .get_authorization_code_client("google")
        .await
        .map_err(|e| {
            tracing::error!("Error getting client: {:?}", e);
            Error::InternalServerError
        })?;
    // Get JWT secret from the config
    let jwt_secret = ctx.config.get_jwt_config()?;
    let user = callback_jwt::<OAuth2UserProfile, users::Model, o_auth2_sessions::Model, SessionNullPool>(&ctx, session, params, &mut client).await?;
    drop(client);
    let token = user
        .generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
        .or_else(|_| unauthorized("unauthorized!"))?;
    // Return jwt token
    Ok(token)
}

ProtectedController 示例

// src/controllers/oauth2.rs
use loco_rs::prelude::*;
use crate::{
    models::{o_auth2_sessions, users, users::OAuth2UserProfile},
    views::auth::LoginResponse,
};

async fn protected(
    State(ctx): State<AppContext>,
    // Extract the user from the Cookie via middleware
    user: OAuth2CookieUser<OAuth2UserProfile, users::Model, o_auth2_sessions::Model>,
) -> Result<Response> {
    let user: &users::Model = user.as_ref();
    let jwt_secret = ctx.config.get_jwt_config()?;
    // Generate a JWT token
    let token = user
        .generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
        .or_else(|_| unauthorized("unauthorized!"))?;
    // Return the user and the token in JSON format
    format::json(LoginResponse::new(user, &token))
}

由于我们使用Google OAuth2提供者,有一个Google模板代码来获取授权URL和回调Cookie。

// src/controllers/oauth2.rs
use axum_session::SessionNullPool;
use loco_oauth2::controllers::{
    middleware::OAuth2CookieUser,
    oauth2::{google_authorization_url, google_callback_cookie},
};
use loco_rs::prelude::*;

use crate::{
    models::{o_auth2_sessions, users, users::OAuth2UserProfile},
    views::auth::LoginResponse,
};

async fn protected(
    State(ctx): State<AppContext>,
    // Extract the user from the Cookie via middleware
    user: OAuth2CookieUser<OAuth2UserProfile, users::Model, o_auth2_sessions::Model>,
) -> Result<Response> {
    let user: &users::Model = user.as_ref();
    let jwt_secret = ctx.config.get_jwt_config()?;
    // Generate a JWT token
    let token = user
        .generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
        .or_else(|_| unauthorized("unauthorized!"))?;
    // Return the user and the token in JSON format
    format::json(LoginResponse::new(user, &token))
}

pub fn routes() -> Routes {
    Routes::new()
        .prefix("api/oauth2")
        .add("/google", get(google_authorization_url::<SessionNullPool>))
        // Route for the Cookie callback
        .add(
            "/google/callback/cookie",
            get(google_callback_cookie::<
                OAuth2UserProfile,
                users::Model,
                o_auth2_sessions::Model,
                SessionNullPool,
            >),
        )
        .add("/protected", get(protected))
}

Google 模板示例 - JWT

由于我们使用Google OAuth2提供商,因此有一个用于获取授权URL和回调jwt的Google模板代码。

// src/controllers/oauth2.rs
use axum_session::SessionNullPool;
use loco_oauth2::controllers::{
    oauth2::{google_authorization_url, google_callback_jwt},
};
use loco_rs::prelude::*;

use crate::{
    models::{o_auth2_sessions, users, users::OAuth2UserProfile},
    views::auth::LoginResponse,
};


pub fn routes() -> Routes {
    Routes::new()
        .prefix("api/oauth2")
        .add("/google", get(google_authorization_url::<SessionNullPool>))
        // Route for the JWT callback
        .add(
            "/google/callback/jwt",
            get(google_callback_jwt::<
                OAuth2UserProfile,
                users::Model,
                o_auth2_sessions::Model,
                SessionNullPool,
            >),
        )
}

依赖项

~76–110MB
~2M SLoC