Skip to main content

auth/
public.rs

1use crate::resolver::session_token_hash;
2use crate::session_policy::{AllowSessionPolicy, AuthSessionPolicy, SessionCreateInput};
3use chrono::{DateTime, Utc};
4use platform_core::{AppError, AppResult, DbPool, ErrorCode};
5use sqlx::{Postgres, Transaction};
6
7pub use crate::models::{AuthSession, AuthUserId};
8pub use crate::session_policy::SessionCreateOptions;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct AuthIdentity {
12    pub id: String,
13    pub user_id: AuthUserId,
14}
15
16pub async fn create_user_identity_in_tx(
17    tx: &mut Transaction<'_, Postgres>,
18    user_id: AuthUserId,
19    identity_id: String,
20    provider: &str,
21    provider_subject: &str,
22    created_at: DateTime<Utc>,
23) -> AppResult<AuthIdentity> {
24    sqlx::query(
25        r#"
26        insert into auth.users (id, created_at, disabled_at, disabled_reason, disabled_until)
27        values ($1, $2, null, null, null)
28        "#,
29    )
30    .bind(&user_id.0)
31    .bind(created_at)
32    .execute(&mut **tx)
33    .await
34    .map_err(map_sql_error)?;
35
36    sqlx::query(
37        r#"
38        insert into auth.identities (id, user_id, provider, provider_subject, created_at, updated_at)
39        values ($1, $2, $3, $4, $5, $5)
40        "#,
41    )
42    .bind(&identity_id)
43    .bind(&user_id.0)
44    .bind(provider)
45    .bind(provider_subject)
46    .bind(created_at)
47    .execute(&mut **tx)
48    .await
49    .map_err(map_sql_error)?;
50
51    Ok(AuthIdentity {
52        id: identity_id,
53        user_id,
54    })
55}
56
57pub async fn find_active_identity(
58    pool: &DbPool,
59    provider: &str,
60    provider_subject: &str,
61) -> AppResult<Option<AuthIdentity>> {
62    sqlx::query_as::<_, IdentityRow>(
63        r#"
64        select identities.id, identities.user_id
65        from auth.identities identities
66        join auth.users users on users.id = identities.user_id
67        where identities.provider = $1
68          and identities.provider_subject = $2
69          and (users.disabled_at is null or users.disabled_until <= now())
70        limit 1
71        "#,
72    )
73    .bind(provider)
74    .bind(provider_subject)
75    .fetch_optional(pool)
76    .await
77    .map(|row| row.map(identity_from_row))
78    .map_err(map_sql_error)
79}
80
81pub async fn create_session(
82    pool: &DbPool,
83    user_id: &AuthUserId,
84    session_id: String,
85    token: String,
86    created_at: DateTime<Utc>,
87    expires_at: DateTime<Utc>,
88) -> AppResult<AuthSession> {
89    create_session_with_policy(
90        pool,
91        user_id,
92        session_id,
93        token,
94        created_at,
95        expires_at,
96        SessionCreateOptions::default(),
97        &AllowSessionPolicy,
98    )
99    .await
100}
101
102pub async fn create_session_with_policy(
103    pool: &DbPool,
104    user_id: &AuthUserId,
105    session_id: String,
106    token: String,
107    created_at: DateTime<Utc>,
108    expires_at: DateTime<Utc>,
109    options: SessionCreateOptions,
110    policy: &dyn AuthSessionPolicy,
111) -> AppResult<AuthSession> {
112    let mut tx = pool.begin().await.map_err(map_sql_error)?;
113    let session = create_session_in_tx_with_policy(
114        &mut tx, user_id, session_id, token, created_at, expires_at, options, policy,
115    )
116    .await?;
117    tx.commit().await.map_err(map_sql_error)?;
118    Ok(session)
119}
120
121pub async fn create_session_in_tx(
122    tx: &mut Transaction<'_, Postgres>,
123    user_id: &AuthUserId,
124    session_id: String,
125    token: String,
126    created_at: DateTime<Utc>,
127    expires_at: DateTime<Utc>,
128) -> AppResult<AuthSession> {
129    create_session_in_tx_with_policy(
130        tx,
131        user_id,
132        session_id,
133        token,
134        created_at,
135        expires_at,
136        SessionCreateOptions::default(),
137        &AllowSessionPolicy,
138    )
139    .await
140}
141
142pub async fn create_session_in_tx_with_policy(
143    tx: &mut Transaction<'_, Postgres>,
144    user_id: &AuthUserId,
145    session_id: String,
146    token: String,
147    created_at: DateTime<Utc>,
148    expires_at: DateTime<Utc>,
149    options: SessionCreateOptions,
150    policy: &dyn AuthSessionPolicy,
151) -> AppResult<AuthSession> {
152    let active_user_exists = sqlx::query_scalar::<_, bool>(
153        r#"
154        select exists(
155            select 1
156            from auth.users
157            where id = $1
158              and (disabled_at is null or disabled_until <= now())
159        )
160        "#,
161    )
162    .bind(&user_id.0)
163    .fetch_one(&mut **tx)
164    .await
165    .map_err(map_sql_error)?;
166
167    if !active_user_exists {
168        return Err(AppError::new(ErrorCode::Forbidden, "Auth user is disabled"));
169    }
170
171    let decision = policy
172        .before_session_create(&SessionCreateInput {
173            user_id: user_id.clone(),
174            session_id: session_id.clone(),
175            proposed_device_id: options.device_id,
176            created_at,
177            expires_at,
178            client: options.client.clone(),
179        })
180        .await?;
181
182    sqlx::query(
183        r#"
184        insert into auth.sessions (
185            id,
186            user_id,
187            token_hash,
188            device_id,
189            client_ip,
190            user_agent,
191            created_at,
192            expires_at,
193            revoked_at
194        )
195        values ($1, $2, $3, $4, $5, $6, $7, $8, null)
196        "#,
197    )
198    .bind(&session_id)
199    .bind(&user_id.0)
200    .bind(session_token_hash(&token))
201    .bind(decision.device_id.as_deref())
202    .bind(options.client.ip.as_deref())
203    .bind(options.client.user_agent.as_deref())
204    .bind(created_at)
205    .bind(expires_at)
206    .execute(&mut **tx)
207    .await
208    .map_err(map_sql_error)?;
209
210    Ok(AuthSession {
211        id: session_id,
212        user_id: user_id.clone(),
213        token,
214        device_id: decision.device_id,
215        expires_at,
216    })
217}
218
219type IdentityRow = (String, String);
220
221fn identity_from_row(row: IdentityRow) -> AuthIdentity {
222    let (id, user_id) = row;
223    AuthIdentity {
224        id,
225        user_id: AuthUserId(user_id),
226    }
227}
228
229fn map_sql_error(source: sqlx::Error) -> AppError {
230    if let sqlx::Error::Database(database_error) = &source {
231        if database_error.constraint() == Some("identities_provider_subject_key") {
232            return AppError::new(ErrorCode::Conflict, "An auth identity already exists")
233                .with_source(source);
234        }
235    }
236
237    AppError::new(ErrorCode::Internal, "Internal server error").with_source(source)
238}