Bläddra i källkod

Refactoring and add untested direct grant.

Kestrel 11 månader sedan
förälder
incheckning
30d6ca7a8f
7 ändrade filer med 381 tillägg och 354 borttagningar
  1. 1 2
      src/client_management.rs
  2. 0 1
      src/key.rs
  3. 2 3
      src/schema.rs
  4. 1 6
      src/server.rs
  5. 5 342
      src/server/oidc.rs
  6. 165 0
      src/server/oidc/authorize.rs
  7. 207 0
      src/server/oidc/token.rs

+ 1 - 2
src/client_management.rs

@@ -8,7 +8,6 @@ pub fn create(
 ) -> Result<(), UIDCError> {
     let rng = ring::rand::SystemRandom::new();
     let client_secret: [u8; 32] = ring::rand::generate(&rng).unwrap().expose();
-    let refresh_token_secret: [u8; 32] = ring::rand::generate(&rng).unwrap().expose();
 
     realm.clients.insert(schema::Client {
         realm: realm.id(),
@@ -16,7 +15,7 @@ pub fn create(
         secret: base64::encode(&client_secret),
         access_key_type: key_type.into(),
         refresh_key_type: KeyType::HMac(HMacType::Sha256).into_serialized(),
-        refresh_token_secret: refresh_token_secret.into(),
+        direct_grant_enabled: false,
         redirects: Default::default(),
         scopes: Default::default(),
     })?;

+ 0 - 1
src/key.rs

@@ -1,5 +1,4 @@
 use crate::{schema, UIDCError};
-use hmac::{Hmac, Mac};
 use microrm::prelude::*;
 use ring::{
     rand::SecureRandom,

+ 2 - 3
src/schema.rs

@@ -154,12 +154,11 @@ pub struct Client {
 
     pub secret: String,
 
-    #[elide]
-    pub refresh_token_secret: Vec<u8>,
-
     pub access_key_type: microrm::Serialized<KeyType>,
     pub refresh_key_type: microrm::Serialized<KeyType>,
 
+    pub direct_grant_enabled: bool,
+
     pub redirects: microrm::RelationMap<ClientRedirect>,
     pub scopes: microrm::RelationMap<Scope>,
 }

+ 1 - 6
src/server.rs

@@ -1,9 +1,4 @@
-use std::{
-    collections::HashMap,
-    sync::{Arc, Mutex, RwLock},
-};
-
-use crate::{config, key, schema, UIDCError, realm};
+use crate::{config, schema, UIDCError, realm};
 
 mod oidc;
 mod session;

+ 5 - 342
src/server/oidc.rs

@@ -1,10 +1,10 @@
-use crate::{config, key, schema, UIDCError};
+use crate::{key, schema, server::session::SessionHelper};
 use microrm::prelude::*;
 use serde::{Deserialize, Serialize};
 
-use super::session::SessionHelper;
-
 mod api;
+mod authorize;
+mod token;
 
 type Request = tide::Request<super::ServerStateWrapper>;
 
@@ -58,176 +58,6 @@ impl<'a> From<microrm::Error> for OIDCError<'a> {
     }
 }
 
-#[derive(Deserialize)]
-struct AuthorizeQueryParams {
-    response_type: String,
-    client_id: String,
-    redirect_uri: String,
-    scope: Option<String>,
-}
-
-fn do_code_authorize<'l, 's>(
-    config: &config::Config,
-    realm: &microrm::Stored<schema::Realm>,
-    client: &microrm::Stored<schema::Client>,
-    user: &microrm::Stored<schema::User>,
-    scopes: impl Iterator<Item = &'l str>,
-    redirect_uri: String,
-    state: Option<&'s str>,
-) -> Result<tide::Response, OIDCError<'s>> {
-    let expiry =
-        std::time::SystemTime::now() + std::time::Duration::from_secs(config.auth_code_expiry);
-
-    let rng = ring::rand::SystemRandom::new();
-    let raw_auth_code: [u8; 32] = ring::rand::generate(&rng)
-        .map_err(|_| {
-            OIDCError(
-                OIDCErrorType::ServerError,
-                format!("Failed to generate auth code."),
-                state,
-            )
-        })?
-        .expose();
-    let encoded_auth_code = base64::encode_config(raw_auth_code, base64::URL_SAFE_NO_PAD);
-
-    realm.auth_codes.insert(schema::AuthCode {
-        realm: realm.id(),
-        client: client.id(),
-        user: user.id(),
-        scopes: scopes
-            .map(|x| x.to_owned())
-            .collect::<Vec<_>>()
-            .into_serialized(),
-        expiry: expiry.into(),
-        redirect_uri: redirect_uri.clone(),
-        code: encoded_auth_code.clone(),
-    })?;
-
-    let new_params = [
-        ("response_type", "code"),
-        ("code", encoded_auth_code.as_str()),
-    ]
-    .into_iter()
-    .chain(state.map(|v| ("state", v)));
-
-    Ok(tide::Redirect::temporary(
-        tide::http::Url::parse_with_params(redirect_uri.as_str(), new_params).map_err(|e| {
-            OIDCError(
-                OIDCErrorType::InvalidRequest,
-                format!("could not parse redirect_uri as a URL: {e}"),
-                None,
-            )
-        })?,
-    )
-    .into())
-}
-
-fn do_authorize(request: Request, state: Option<&str>) -> Result<tide::Response, OIDCError> {
-    let shelper = SessionHelper::new(&request);
-    let realm = shelper.get_realm().map_err(|_| {
-        OIDCError(
-            OIDCErrorType::InvalidRequest,
-            "No such realm!".to_string(),
-            state,
-        )
-    })?;
-
-    let make_redirect = || {
-        let mut login_url = request.url().join("../v1/session/login").unwrap();
-        login_url
-            .query_pairs_mut()
-            .clear()
-            .append_pair("redirect", request.url().as_str());
-        return Ok(tide::Redirect::new(login_url).into());
-    };
-
-    let qp: AuthorizeQueryParams = request
-        .query()
-        .map_err(|x| OIDCError(OIDCErrorType::InvalidRequest, x.to_string(), state))?;
-
-    // collect session authentication info
-
-    let potential_sauth = shelper
-        .get_session(&request)
-        .and_then(|session| shelper.get_auth_for_session(realm.id(), &session));
-
-    let Some(user_id) = potential_sauth.and_then(|v| v.user) else {
-        // if we don't have any relevant auth info, redirect to login
-        return make_redirect();
-    };
-
-    let Ok(Some(user)) = realm
-        .users
-        .with(schema::UserID::default(), user_id)
-        .first()
-        .get()
-    else {
-        return Err(OIDCError(
-            OIDCErrorType::ServerError,
-            "Internal state error!".to_string(),
-            state,
-        ));
-    };
-
-    // verify the realm and client_id and redirect_uri
-
-    let client = realm
-        .clients
-        .with(schema::Client::Shortname, &qp.client_id)
-        .first()
-        .get()
-        .ok()
-        .flatten()
-        .ok_or_else(|| {
-            OIDCError(
-                OIDCErrorType::UnauthorizedClient,
-                "Client does not exist".to_string(),
-                state,
-            )
-        })?;
-
-    let scopes = qp
-        .scope
-        .as_ref()
-        .map(|slist| slist.as_str())
-        .unwrap_or("")
-        .split_whitespace();
-
-    // TODO: check that redirect URI matches
-
-    if qp.response_type == "code" {
-        do_code_authorize(
-            &request.state().core.config,
-            &realm,
-            &client,
-            &user,
-            scopes,
-            qp.redirect_uri,
-            state,
-        )
-    } else if qp.response_type == "token" {
-        let rhelper = request.state().core.realms.get_helper(realm.id()).unwrap();
-
-        let token = rhelper.generate_access_token(&client, &user, scopes).map_err(|e| OIDCError(OIDCErrorType::ServerError, format!("could not generate token: {e}"), state))?;
-
-        Ok(tide::Response::builder(200)
-           .content_type(tide::http::mime::JSON)
-           .body(serde_json::to_vec(&api::TokenResponse {
-                token_type: "bearer",
-                access_token: token.as_str(),
-                refresh_token: None,
-                scope: None,
-           }).unwrap())
-           .build())
-    } else {
-        Err(OIDCError(
-            OIDCErrorType::UnsupportedResponseType,
-            "Only code and token are understood.".to_string(),
-            state,
-        ))
-    }
-}
-
 async fn authorize(request: Request) -> tide::Result<tide::Response> {
     #[derive(Deserialize)]
     struct State {
@@ -235,141 +65,14 @@ async fn authorize(request: Request) -> tide::Result<tide::Response> {
     }
     let state: Option<String> = request.query::<State>().ok().map(|x| x.state).flatten();
 
-    match do_authorize(request, state.as_ref().map(|x| x.as_str())) {
+    match authorize::do_authorize(request, state.as_ref().map(|x| x.as_str())) {
         Ok(r) => Ok(r),
         Err(e) => Ok(e.to_response()),
     }
 }
 
-async fn do_token<'l>(mut request: Request) -> Result<tide::Response, OIDCError<'l>> {
-    let shelper = SessionHelper::new(&request);
-    let realm = shelper
-        .get_realm()
-        .map_err(|_| OIDCError(OIDCErrorType::InvalidRequest, "no such realm".into(), None))?;
-
-    let treq: api::TokenRequestBody = request.body_form().await.map_err(|e| {
-        OIDCError(
-            OIDCErrorType::InvalidRequest,
-            format!("could not parse form body: {e}"),
-            None,
-        )
-    })?;
-
-    // TODO: support HTTP basic auth for client authentication instead of treq.client_id
-    let client_name = treq.client_id.ok_or(OIDCError(
-        OIDCErrorType::InvalidRequest,
-        "no client given".into(),
-        None,
-    ))?;
-    let client = realm
-        .clients
-        .keyed((realm.id(), &client_name))
-        .get()?
-        .ok_or(OIDCError(
-            OIDCErrorType::InvalidRequest,
-            format!("unknown client name {client_name}"),
-            None,
-        ))?;
-
-    let rhelper = request.state().core.realms.get_helper(realm.id()).unwrap();
-
-    if treq.grant_type == "authorization_code" {
-        let Some(code) = treq.code else { todo!() };
-        let code = realm
-            .auth_codes
-            .keyed((realm.id(), client.id(), code))
-            .get()?
-            .ok_or(OIDCError(
-                OIDCErrorType::InvalidRequest,
-                "invalid authorization code".into(),
-                None,
-            ))?;
-
-        let now = std::time::SystemTime::now();
-        if code.expiry < now {
-            return Err(OIDCError(
-                OIDCErrorType::AccessDenied,
-                "expired authorization code".into(),
-                None,
-            ));
-        }
-
-        let user = realm
-            .users
-            .with(schema::UserID::default(), code.user)
-            .first()
-            .get()?
-            .ok_or(OIDCError(
-                OIDCErrorType::ServerError,
-                "could not find user".into(),
-                None,
-            ))?;
-
-        let access_token = rhelper.generate_access_token(
-            &client,
-            &user,
-            code.scopes.as_ref().iter().map(String::as_str),
-        )
-        .map_err(|e| {
-            OIDCError(
-                OIDCErrorType::ServerError,
-                format!("error signing key: {e}"),
-                None,
-            )
-        })?;
-
-        Ok(tide::Response::builder(200)
-            .content_type(tide::http::mime::JSON)
-            .body(
-                serde_json::to_value(api::TokenResponse {
-                    access_token: access_token.as_str(),
-                    token_type: "bearer",
-                    refresh_token: None,
-                    scope: None,
-                })
-                .unwrap(),
-            )
-            .build())
-    } else if treq.grant_type == "refresh_token" {
-        let Some(rtoken) = treq.refresh_token else {
-            return Err(OIDCError(
-                OIDCErrorType::InvalidRequest,
-                "no refresh_token given".into(),
-                None,
-            ));
-        };
-
-        let (access, refresh) = match rhelper.trade_refresh_token(&client, rtoken.as_str()) {
-            Ok((a, r)) => (a, r),
-            Err(e) => return Err(OIDCError(
-                OIDCErrorType::InvalidRequest,
-                format!("could not trade refresh token: {e}"),
-                None,
-            ))
-        };
-        Ok(tide::Response::builder(200)
-            .content_type(tide::http::mime::JSON)
-            .body(
-                serde_json::to_value(api::TokenResponse {
-                    access_token: access.as_str(),
-                    token_type: "bearer",
-                    refresh_token: Some(refresh.as_str()),
-                    scope: None,
-                })
-                .unwrap(),
-            )
-            .build())
-    } else {
-        Err(OIDCError(
-            OIDCErrorType::InvalidRequest,
-            format!("unknown grant type {}", treq.grant_type),
-            None,
-        ))
-    }
-}
-
 async fn token(request: Request) -> tide::Result<tide::Response> {
-    match do_token(request).await {
+    match token::do_token(request).await {
         Ok(res) => Ok(res),
         Err(e) => Ok(e.to_response()),
     }
@@ -395,46 +98,6 @@ async fn jwks(request: Request) -> tide::Result<tide::Response> {
         jwkset.keys.push(key.wrapped().into_jwk());
     }
 
-    /*let keyinfo =
-        realm
-            .keys
-            .get()?
-            .into_iter()
-            .map(|key| match key::ParsedKey::parse_from(&key)? {
-                key::ParsedKey::HMAC { key_id, hmty, .. } => Ok(serde_json::json!({
-                    "kid": key_id,
-                    "alg": match hmty {
-                        key::HMacType::Sha256 => "HS256",
-                        key::HMacType::Sha512 => "HS512",
-                    },
-                    "hmac": true,
-                })),
-                key::ParsedKey::Ed25519 { key_id, keypair } => Ok(serde_json::json!({
-                    "crv": "Ed25519",
-                    "kid": key_id,
-                    "kty": "OKP",
-                    "use": "sig",
-
-                    "x": base64::encode(keypair.public_key().as_ref()),
-                })),
-                key::ParsedKey::RSA { key_id, keypair } => {
-                    let pubkey = keypair.public_key();
-                    Ok(serde_json::json!({
-                        "alg": "RS256",
-                        "kid": key_id,
-                        "kty": "RSA",
-                        "use": "sig",
-
-                        "e": base64::encode(pubkey.exponent().big_endian_without_leading_zero()),
-                        "n": base64::encode(pubkey.modulus().big_endian_without_leading_zero()),
-                    }))
-                }
-            });
-
-    let jwks_response = serde_json::json!({
-        "keys": keyinfo.collect::<Result<Vec<_>, UIDCError>>()?,
-    });*/
-
     Ok(tide::Response::builder(200)
         .header(tide::http::headers::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
         .content_type(tide::http::mime::JSON)

+ 165 - 0
src/server/oidc/authorize.rs

@@ -0,0 +1,165 @@
+use microrm::prelude::*;
+use crate::{schema,config,server::session::SessionHelper};
+use super::{api,OIDCError,OIDCErrorType,Request};
+
+fn do_code_authorize<'l, 's>(
+    config: &config::Config,
+    realm: &microrm::Stored<schema::Realm>,
+    client: &microrm::Stored<schema::Client>,
+    user: &microrm::Stored<schema::User>,
+    scopes: impl Iterator<Item = &'l str>,
+    redirect_uri: String,
+    state: Option<&'s str>,
+) -> Result<tide::Response, OIDCError<'s>> {
+    let expiry =
+        std::time::SystemTime::now() + std::time::Duration::from_secs(config.auth_code_expiry);
+
+    let rng = ring::rand::SystemRandom::new();
+    let raw_auth_code: [u8; 32] = ring::rand::generate(&rng)
+        .map_err(|_| {
+            OIDCError(
+                OIDCErrorType::ServerError,
+                format!("Failed to generate auth code."),
+                state,
+            )
+        })?
+        .expose();
+    let encoded_auth_code = base64::encode_config(raw_auth_code, base64::URL_SAFE_NO_PAD);
+
+    realm.auth_codes.insert(schema::AuthCode {
+        realm: realm.id(),
+        client: client.id(),
+        user: user.id(),
+        scopes: scopes
+            .map(|x| x.to_owned())
+            .collect::<Vec<_>>()
+            .into_serialized(),
+        expiry: expiry.into(),
+        redirect_uri: redirect_uri.clone(),
+        code: encoded_auth_code.clone(),
+    })?;
+
+    let new_params = [
+        ("response_type", "code"),
+        ("code", encoded_auth_code.as_str()),
+    ]
+    .into_iter()
+    .chain(state.map(|v| ("state", v)));
+
+    Ok(tide::Redirect::temporary(
+        tide::http::Url::parse_with_params(redirect_uri.as_str(), new_params).map_err(|e| {
+            OIDCError(
+                OIDCErrorType::InvalidRequest,
+                format!("could not parse redirect_uri as a URL: {e}"),
+                None,
+            )
+        })?,
+    )
+    .into())
+}
+
+pub(super) fn do_authorize(request: Request, state: Option<&str>) -> Result<tide::Response, OIDCError> {
+    let shelper = SessionHelper::new(&request);
+    let realm = shelper.get_realm().map_err(|_| {
+        OIDCError(
+            OIDCErrorType::InvalidRequest,
+            "No such realm!".to_string(),
+            state,
+        )
+    })?;
+
+    let make_redirect = || {
+        let mut login_url = request.url().join("../v1/session/login").unwrap();
+        login_url
+            .query_pairs_mut()
+            .clear()
+            .append_pair("redirect", request.url().as_str());
+        return Ok(tide::Redirect::new(login_url).into());
+    };
+
+    let qp: api::AuthorizationRequestQuery = request
+        .query()
+        .map_err(|x| OIDCError(OIDCErrorType::InvalidRequest, x.to_string(), state))?;
+
+    // collect session authentication info
+
+    let potential_sauth = shelper
+        .get_session(&request)
+        .and_then(|session| shelper.get_auth_for_session(realm.id(), &session));
+
+    let Some(user_id) = potential_sauth.and_then(|v| v.user) else {
+        // if we don't have any relevant auth info, redirect to login
+        return make_redirect();
+    };
+
+    let Ok(Some(user)) = realm
+        .users
+        .with(schema::UserID::default(), user_id)
+        .first()
+        .get()
+    else {
+        return Err(OIDCError(
+            OIDCErrorType::ServerError,
+            "Internal state error!".to_string(),
+            state,
+        ));
+    };
+
+    // verify the realm and client_id and redirect_uri
+
+    let client = realm
+        .clients
+        .with(schema::Client::Shortname, &qp.client_id)
+        .first()
+        .get()
+        .ok()
+        .flatten()
+        .ok_or_else(|| {
+            OIDCError(
+                OIDCErrorType::UnauthorizedClient,
+                "Client does not exist".to_string(),
+                state,
+            )
+        })?;
+
+    let scopes = qp
+        .scope
+        .as_ref()
+        .map(|slist| slist.as_str())
+        .unwrap_or("")
+        .split_whitespace();
+
+    // TODO: check that redirect URI matches
+
+    if qp.response_type == "code" {
+        do_code_authorize(
+            &request.state().core.config,
+            &realm,
+            &client,
+            &user,
+            scopes,
+            qp.redirect_uri,
+            state,
+        )
+    } else if qp.response_type == "token" {
+        let rhelper = request.state().core.realms.get_helper(realm.id()).unwrap();
+
+        let token = rhelper.generate_access_token(&client, &user, scopes).map_err(|e| OIDCError(OIDCErrorType::ServerError, format!("could not generate token: {e}"), state))?;
+
+        Ok(tide::Response::builder(200)
+           .content_type(tide::http::mime::JSON)
+           .body(serde_json::to_vec(&api::TokenResponse {
+                token_type: "bearer",
+                access_token: token.as_str(),
+                refresh_token: None,
+                scope: None,
+           }).unwrap())
+           .build())
+    } else {
+        Err(OIDCError(
+            OIDCErrorType::UnsupportedResponseType,
+            "Only code and token are understood.".to_string(),
+            state,
+        ))
+    }
+}

+ 207 - 0
src/server/oidc/token.rs

@@ -0,0 +1,207 @@
+use microrm::prelude::*;
+use crate::{realm::RealmHelper, schema, server::session::SessionHelper, user::{UserError, UserExt}, UIDCError};
+use super::{api,OIDCError,OIDCErrorType,Request};
+
+fn do_authorization_code<'l>(realm: &microrm::Stored<schema::Realm>, rhelper: &RealmHelper, client: &microrm::Stored<schema::Client>, treq: &api::TokenRequestBody) -> Result<tide::Response, OIDCError<'l>> {
+    let Some(code) = treq.code.as_ref() else {
+        return Err(OIDCError(OIDCErrorType::InvalidRequest, "no authorization code provided".into(), None))
+    };
+    let code = realm
+        .auth_codes
+        .keyed((realm.id(), client.id(), code))
+        .get()?
+        .ok_or(OIDCError(
+            OIDCErrorType::AccessDenied,
+            "invalid authorization code".into(),
+            None,
+        ))?;
+
+    let now = std::time::SystemTime::now();
+    if code.expiry < now {
+        return Err(OIDCError(
+            OIDCErrorType::AccessDenied,
+            "expired authorization code".into(),
+            None,
+        ));
+    }
+
+    let user = realm
+        .users
+        .with(schema::UserID::default(), code.user)
+        .first()
+        .get()?
+        .ok_or(OIDCError(
+            OIDCErrorType::ServerError,
+            "could not find user".into(),
+            None,
+        ))?;
+
+    let access_token = rhelper.generate_access_token(
+        &client,
+        &user,
+        code.scopes.as_ref().iter().map(String::as_str),
+    )
+    .map_err(|e| {
+        OIDCError(
+            OIDCErrorType::ServerError,
+            format!("error signing key: {e}"),
+            None,
+        )
+    })?;
+
+    Ok(tide::Response::builder(200)
+        .content_type(tide::http::mime::JSON)
+        .body(
+            serde_json::to_value(api::TokenResponse {
+                access_token: access_token.as_str(),
+                token_type: "bearer",
+                refresh_token: None,
+                scope: None,
+            })
+            .unwrap(),
+        )
+        .build())
+}
+
+fn do_refresh_token<'l>(_realm: &microrm::Stored<schema::Realm>, rhelper: &RealmHelper, client: &microrm::Stored<schema::Client>, treq: &api::TokenRequestBody) -> Result<tide::Response, OIDCError<'l>> {
+    let Some(rtoken) = treq.refresh_token.as_ref() else {
+        return Err(OIDCError(
+            OIDCErrorType::InvalidRequest,
+            "no refresh_token given".into(),
+            None,
+        ));
+    };
+
+    let (access, refresh) = match rhelper.trade_refresh_token(&client, rtoken.as_str()) {
+        Ok((a, r)) => (a, r),
+        Err(e) => return Err(OIDCError(
+            OIDCErrorType::InvalidRequest,
+            format!("could not trade refresh token: {e}"),
+            None,
+        ))
+    };
+    Ok(tide::Response::builder(200)
+        .content_type(tide::http::mime::JSON)
+        .body(
+            serde_json::to_value(api::TokenResponse {
+                access_token: access.as_str(),
+                token_type: "bearer",
+                refresh_token: Some(refresh.as_str()),
+                scope: None,
+            })
+            .unwrap(),
+        )
+        .build())
+}
+
+fn do_direct_grant<'l>(realm: &microrm::Stored<schema::Realm>, rhelper: &RealmHelper, client: &microrm::Stored<schema::Client>, treq: &api::TokenRequestBody) -> Result<tide::Response, OIDCError<'l>> {
+    // first thing to check: does the client have the direct grant type enabled?
+    if !client.direct_grant_enabled {
+        return Err(OIDCError(OIDCErrorType::UnsupportedResponseType, "client does not have direct grants enabled".into(), None))
+    }
+
+    let Some(username) = treq.username.as_ref() else {
+        return Err(OIDCError(OIDCErrorType::InvalidRequest, "no username provided".into(), None))
+    };
+    let Some(password) = treq.password.as_ref() else {
+        return Err(OIDCError(OIDCErrorType::InvalidRequest, "no password provided".into(), None))
+    };
+
+    let Some(user) = realm.users.with(schema::User::Username, username).first().get()? else {
+        return Err(OIDCError(OIDCErrorType::AccessDenied, "no such user".into(), None))
+    };
+
+    // verify that we don't accidentally log in someone with username/password who has MFA..
+    match user.auth.with(schema::AuthChallenge::Enabled, true).count()? {
+        0 => return Err(OIDCError(OIDCErrorType::AccessDenied, "user has no associated password".into(), None)),
+        1 => (),
+        _ => return Err(OIDCError(OIDCErrorType::AccessDenied, "user has MFA enabled".into(), None)),
+    }
+
+    match user.verify_challenge_by_type(schema::AuthChallengeType::Password, password.as_bytes()) {
+        Err(UIDCError::UserError(UserError::NoSuchChallenge)) => {
+            Err(OIDCError(OIDCErrorType::AccessDenied, "user has no associated password".into(), None))
+        },
+        Err(UIDCError::DatabaseError(e)) => {
+            Err(e.into())
+        },
+        Err(e) => {
+            Err(OIDCError(OIDCErrorType::ServerError, format!("internal error: {e}"), None))
+        },
+        Ok(false) => {
+            Err(OIDCError(OIDCErrorType::AccessDenied, "password authentication failed".into(), None))
+        },
+        Ok(true) => {
+            let scopes = treq
+                .scope
+                .as_ref()
+                .map(String::as_str)
+                .unwrap_or("")
+                .split_whitespace();
+            let Ok(access_token) = rhelper.generate_access_token(client, &user, scopes.clone()) else {
+                return Err(OIDCError(OIDCErrorType::ServerError, "could not generate access token".into(), None))
+            };
+            let Ok(refresh_token) = rhelper.generate_refresh_token(client, &user, scopes) else {
+                return Err(OIDCError(OIDCErrorType::ServerError, "could not generate access token".into(), None))
+            };
+
+            Ok(tide::Response::builder(200)
+               .content_type(tide::http::mime::JSON)
+               .body(serde_json::to_vec(&api::TokenResponse {
+                    access_token: access_token.as_str(),
+                    token_type: "bearer",
+                    refresh_token: Some(refresh_token.as_str()),
+                    scope: None,
+               }).unwrap())
+               .build())
+        },
+    }
+}
+
+pub(super) async fn do_token<'l>(mut request: Request) -> Result<tide::Response, OIDCError<'l>> {
+    let shelper = SessionHelper::new(&request);
+    let realm = shelper
+        .get_realm()
+        .map_err(|_| OIDCError(OIDCErrorType::InvalidRequest, "no such realm".into(), None))?;
+
+    let treq: api::TokenRequestBody = request.body_form().await.map_err(|e| {
+        OIDCError(
+            OIDCErrorType::InvalidRequest,
+            format!("could not parse form body: {e}"),
+            None,
+        )
+    })?;
+
+    // TODO: support HTTP basic auth for client authentication instead of treq.client_id
+    let client_name = treq.client_id.as_ref().ok_or(OIDCError(
+        OIDCErrorType::InvalidRequest,
+        "no client given".into(),
+        None,
+    ))?;
+    let client = realm
+        .clients
+        .keyed((realm.id(), client_name))
+        .get()?
+        .ok_or(OIDCError(
+            OIDCErrorType::InvalidRequest,
+            format!("unknown client name {client_name}"),
+            None,
+        ))?;
+
+    let rhelper = request.state().core.realms.get_helper(realm.id()).unwrap();
+
+    if treq.grant_type == "authorization_code" {
+        do_authorization_code(&realm, rhelper.as_ref(), &client, &treq)
+    } else if treq.grant_type == "refresh_token" {
+        do_refresh_token(&realm, rhelper.as_ref(), &client, &treq)
+    } else if treq.grant_type == "password" {
+        do_direct_grant(&realm, rhelper.as_ref(), &client, &treq)
+    } else {
+        Err(OIDCError(
+            OIDCErrorType::InvalidRequest,
+            format!("unknown grant type {}", treq.grant_type),
+            None,
+        ))
+    }
+}
+