Przeglądaj źródła

Cleanups, support client name from HTTP auth header.

Kestrel 4 dni temu
rodzic
commit
0099e9bc32
9 zmienionych plików z 323 dodań i 282 usunięć
  1. 0 38
      src/cli.rs
  2. 1 1
      src/ext.rs
  3. 3 3
      src/ext/github.rs
  4. 8 222
      src/schema.rs
  5. 224 0
      src/schema/v1.rs
  6. 4 4
      src/server/oidc.rs
  7. 82 12
      src/server/oidc/token.rs
  8. 1 1
      src/server/session.rs
  9. 0 1
      src/server/um.rs

+ 0 - 38
src/cli.rs

@@ -216,44 +216,6 @@ impl KeyArgs {
     }
 }
 
-/*
-#[derive(Debug, Subcommand)]
-enum ConfigCommand {
-    Dump,
-    Load { toml_path: String },
-    Set { key: String, value: String },
-}
-
-#[derive(Debug, Parser)]
-struct ConfigArgs {
-    #[clap(subcommand)]
-    command: ConfigCommand,
-}
-
-impl ConfigArgs {
-    async fn run(self, args: RunArgs) -> Result<(), UIDCError> {
-        match &self.command {
-            ConfigCommand::Dump => {
-                let config = config::Config::build_from(&args.db, None);
-                println!("{:#?}", config);
-            }
-            ConfigCommand::Set { key, value } => {
-                args.db.persistent_config.keyed(key).delete()?;
-                args.db.persistent_config.insert(schema::PersistentConfig {
-                    key: key.clone(),
-                    value: value.clone(),
-                })?;
-            }
-            ConfigCommand::Load { toml_path } => {
-                let config = config::Config::build_from(&args.db, Some(toml_path));
-                config.save(&args.db);
-            }
-        }
-        Ok(())
-    }
-}
-*/
-
 #[derive(Debug, Parser)]
 struct ServerArgs {
     #[clap(short, long)]

+ 1 - 1
src/ext.rs

@@ -92,7 +92,7 @@ pub trait ExternalAuthenticator {
     }
 }
 
-pub use generic_oidc::{OIDCAuthenticator, OIDCConfig};
+pub use generic_oidc::OIDCConfig;
 pub use github::{GithubAuthenticator, GithubConfig};
 use microrm::{
     prelude::{Insertable, Queryable},

+ 3 - 3
src/ext/github.rs

@@ -21,9 +21,9 @@ pub struct GithubConfig {
     pub client_secret: String,
 }
 
-const DEFAULT_LOGIN_URL: &'static str = "https://github.com/login/oauth/authorize";
-const DEFAULT_TOKEN_URL: &'static str = "https://github.com/login/oauth/access_token";
-const DEFAULT_API_BASE: &'static str = "https://api.github.com";
+const DEFAULT_LOGIN_URL: &str = "https://github.com/login/oauth/authorize";
+const DEFAULT_TOKEN_URL: &str = "https://github.com/login/oauth/access_token";
+const DEFAULT_API_BASE: &str = "https://api.github.com";
 
 #[derive(Deserialize)]
 #[serde(rename_all = "lowercase")]

+ 8 - 222
src/schema.rs

@@ -1,227 +1,13 @@
-pub use microrm::prelude::*;
-use serde::{Deserialize, Serialize};
-use strum::EnumString;
+use microrm::prelude::*;
 
-use crate::key::KeyType;
+mod v1;
 
-// ----------------------------------------------------------------------
-// Session types
-// ----------------------------------------------------------------------
-
-#[derive(Entity)]
-pub struct Session {
-    #[key]
-    pub session_id: String,
-    pub auth: microrm::RelationMap<SessionAuth>,
-    pub expiry: time::OffsetDateTime,
-}
-
-#[derive(Entity)]
-pub struct SessionAuth {
-    pub realm: RealmID,
-
-    pub user: Option<UserID>,
-
-    pub pending_user: Option<UserID>,
-    pub pending_challenges: microrm::Serialized<Vec<AuthChallengeType>>,
-}
-
-#[derive(Clone, PartialEq, PartialOrd, Serialize, Deserialize, Debug)]
-pub enum AuthChallengeType {
-    Username,
-    Password,
-    Totp,
-    Grid,
-    WebAuthn,
-}
-
-#[derive(Entity)]
-pub struct AuthChallenge {
-    #[key]
-    pub user_id: UserID,
-    #[key]
-    pub challenge_type: microrm::Serialized<AuthChallengeType>,
-    #[elide]
-    pub public: Vec<u8>,
-    #[elide]
-    pub secret: Vec<u8>,
-    pub enabled: bool,
-}
-
-#[derive(Entity)]
-pub struct SingleUseAuth {
-    #[key]
-    pub code: String,
-    pub user: UserID,
-    pub expiry: time::OffsetDateTime,
-}
-
-// ----------------------------------------------------------------------
-// OIDC types
-// ----------------------------------------------------------------------
-
-pub struct UserGroupRelation;
-impl microrm::Relation for UserGroupRelation {
-    type Domain = User;
-    type Range = Group;
-    const NAME: &'static str = "UserGroup";
-}
-
-pub struct GroupRoleRelation;
-impl microrm::Relation for GroupRoleRelation {
-    type Domain = Group;
-    type Range = Role;
-    const NAME: &'static str = "GroupRole";
-}
-
-#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
-pub enum KeyState {
-    /// Key can be used without restrictions for signing and verification.
-    Active,
-    /// Key will be used for signing only if no other key of the same type is found, but is still
-    /// good for verification of existing tokens.
-    Retiring,
-    /// Key is now fully retired and will not be used for signing or for verification.
-    Retired,
-}
-
-#[derive(Entity)]
-pub struct Key {
-    #[key]
-    pub key_id: String,
-    pub key_type: microrm::Serialized<KeyType>,
-    pub key_state: microrm::Serialized<KeyState>,
-    pub public_data: Vec<u8>,
-    #[elide]
-    pub secret_data: Vec<u8>,
-    pub expiry: time::OffsetDateTime,
-}
-
-#[derive(Entity)]
-pub struct User {
-    #[key]
-    pub realm: RealmID,
-    #[key]
-    pub username: String,
-
-    pub pending_external_auths: microrm::Serialized<Vec<ExternalAuthProvider>>,
-
-    pub auth: microrm::RelationMap<AuthChallenge>,
-    pub groups: microrm::RelationDomain<UserGroupRelation>,
-}
-
-#[derive(Entity)]
-pub struct Group {
-    #[key]
-    pub realm: RealmID,
-    #[key]
-    pub shortname: String,
-    pub users: microrm::RelationRange<UserGroupRelation>,
-    pub roles: microrm::RelationDomain<GroupRoleRelation>,
-}
-
-#[derive(Entity)]
-pub struct Role {
-    #[key]
-    pub realm: RealmID,
-    /// key publicly-visible name for role
-    #[key]
-    pub shortname: String,
-    pub groups: microrm::RelationRange<GroupRoleRelation>,
-}
-
-/// OAuth2 client representation
-#[derive(Entity)]
-pub struct Client {
-    #[key]
-    pub realm: RealmID,
-    #[key]
-    pub shortname: String,
-
-    pub secret: String,
-
-    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>,
-}
-
-#[derive(Entity)]
-pub struct ClientRedirect {
-    pub redirect_pattern: String,
-}
-
-#[derive(Entity)]
-pub struct AuthCode {
-    #[key]
-    pub realm: RealmID,
-    #[key]
-    pub client: ClientID,
-    #[key]
-    pub code: String,
-
-    pub expiry: time::OffsetDateTime,
-
-    pub user: UserID,
-    pub scopes: microrm::Serialized<Vec<String>>,
-    pub redirect_uri: String,
-}
-
-/// Requested group of permissions
-#[derive(Entity)]
-pub struct Scope {
-    #[key]
-    pub realm: RealmID,
-    #[key]
-    pub shortname: String,
-    pub roles: microrm::RelationMap<Role>,
-}
-
-// ----------------------------------------------------------------------
-// External (social) authentication
-// ----------------------------------------------------------------------
-
-#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Serialize, Deserialize, EnumString)]
-#[strum(serialize_all = "snake_case")]
-#[serde(rename_all = "snake_case")]
-pub enum ExternalAuthProvider {
-    Github,
-    GenericOIDC,
-}
-
-#[derive(Clone, Entity)]
-pub struct ExternalAuthMap {
-    #[key]
-    pub external_user_id: String,
-    #[key]
-    pub provider: microrm::Serialized<ExternalAuthProvider>,
-
-    pub internal_user_id: UserID,
-}
-
-// ----------------------------------------------------------------------
-// Global container types
-// ----------------------------------------------------------------------
-
-#[derive(Clone, Default, Entity)]
-pub struct Realm {
-    #[key]
-    pub shortname: String,
-
-    pub clients: microrm::RelationMap<Client>,
-    pub groups: microrm::RelationMap<Group>,
-    pub keys: microrm::RelationMap<Key>,
-    pub roles: microrm::RelationMap<Role>,
-    pub scopes: microrm::RelationMap<Scope>,
-    pub users: microrm::RelationMap<User>,
-    pub auth_codes: microrm::RelationMap<AuthCode>,
-
-    pub external_auth: microrm::RelationMap<ExternalAuthMap>,
-    pub single_use_auth: microrm::RelationMap<SingleUseAuth>,
-}
+#[allow(unused)]
+pub use v1::{
+    AuthChallenge, AuthChallengeType, AuthCode, Client, ClientRedirect, ExternalAuthMap,
+    ExternalAuthProvider, Group, Key, KeyID, KeyState, Realm, RealmID, Role, Scope, Session,
+    SessionAuth, SingleUseAuth, User, UserID,
+};
 
 #[derive(Schema)]
 pub struct UIDCDatabase {

+ 224 - 0
src/schema/v1.rs

@@ -0,0 +1,224 @@
+pub use microrm::prelude::*;
+use serde::{Deserialize, Serialize};
+use strum::EnumString;
+
+use crate::key::KeyType;
+
+// ----------------------------------------------------------------------
+// Session types
+// ----------------------------------------------------------------------
+
+#[derive(Entity)]
+pub struct Session {
+    #[key]
+    pub session_id: String,
+    pub auth: microrm::RelationMap<SessionAuth>,
+    pub expiry: time::OffsetDateTime,
+}
+
+#[derive(Entity)]
+pub struct SessionAuth {
+    pub realm: RealmID,
+
+    pub user: Option<UserID>,
+
+    pub pending_user: Option<UserID>,
+    pub pending_challenges: microrm::Serialized<Vec<AuthChallengeType>>,
+}
+
+#[derive(Clone, PartialEq, PartialOrd, Serialize, Deserialize, Debug)]
+pub enum AuthChallengeType {
+    Username,
+    Password,
+    Totp,
+    Grid,
+    WebAuthn,
+}
+
+#[derive(Entity)]
+pub struct AuthChallenge {
+    #[key]
+    pub user_id: UserID,
+    #[key]
+    pub challenge_type: microrm::Serialized<AuthChallengeType>,
+    #[elide]
+    pub public: Vec<u8>,
+    #[elide]
+    pub secret: Vec<u8>,
+    pub enabled: bool,
+}
+
+#[derive(Entity)]
+pub struct SingleUseAuth {
+    #[key]
+    pub code: String,
+    pub user: UserID,
+    pub expiry: time::OffsetDateTime,
+}
+
+// ----------------------------------------------------------------------
+// OIDC types
+// ----------------------------------------------------------------------
+
+pub struct UserGroupRelation;
+impl microrm::Relation for UserGroupRelation {
+    type Domain = User;
+    type Range = Group;
+    const NAME: &'static str = "UserGroup";
+}
+
+pub struct GroupRoleRelation;
+impl microrm::Relation for GroupRoleRelation {
+    type Domain = Group;
+    type Range = Role;
+    const NAME: &'static str = "GroupRole";
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
+pub enum KeyState {
+    /// Key can be used without restrictions for signing and verification.
+    Active,
+    /// Key will be used for signing only if no other key of the same type is found, but is still
+    /// good for verification of existing tokens.
+    Retiring,
+    /// Key is now fully retired and will not be used for signing or for verification.
+    Retired,
+}
+
+#[derive(Entity)]
+pub struct Key {
+    #[key]
+    pub key_id: String,
+    pub key_type: microrm::Serialized<KeyType>,
+    pub key_state: microrm::Serialized<KeyState>,
+    pub public_data: Vec<u8>,
+    #[elide]
+    pub secret_data: Vec<u8>,
+    pub expiry: time::OffsetDateTime,
+}
+
+#[derive(Entity)]
+pub struct User {
+    #[key]
+    pub realm: RealmID,
+    #[key]
+    pub username: String,
+
+    pub pending_external_auths: microrm::Serialized<Vec<ExternalAuthProvider>>,
+
+    pub auth: microrm::RelationMap<AuthChallenge>,
+    pub groups: microrm::RelationDomain<UserGroupRelation>,
+}
+
+#[derive(Entity)]
+pub struct Group {
+    #[key]
+    pub realm: RealmID,
+    #[key]
+    pub shortname: String,
+    pub users: microrm::RelationRange<UserGroupRelation>,
+    pub roles: microrm::RelationDomain<GroupRoleRelation>,
+}
+
+#[derive(Entity)]
+pub struct Role {
+    #[key]
+    pub realm: RealmID,
+    /// key publicly-visible name for role
+    #[key]
+    pub shortname: String,
+    pub groups: microrm::RelationRange<GroupRoleRelation>,
+}
+
+/// OAuth2 client representation
+#[derive(Entity)]
+pub struct Client {
+    #[key]
+    pub realm: RealmID,
+    #[key]
+    pub shortname: String,
+
+    pub secret: String,
+
+    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>,
+}
+
+#[derive(Entity)]
+pub struct ClientRedirect {
+    pub redirect_pattern: String,
+}
+
+#[derive(Entity)]
+pub struct AuthCode {
+    #[key]
+    pub realm: RealmID,
+    #[key]
+    pub client: ClientID,
+    #[key]
+    pub code: String,
+
+    pub expiry: time::OffsetDateTime,
+
+    pub user: UserID,
+    pub scopes: microrm::Serialized<Vec<String>>,
+    pub redirect_uri: String,
+}
+
+/// Requested group of permissions
+#[derive(Entity)]
+pub struct Scope {
+    #[key]
+    pub realm: RealmID,
+    #[key]
+    pub shortname: String,
+    pub roles: microrm::RelationMap<Role>,
+}
+
+// ----------------------------------------------------------------------
+// External (social) authentication
+// ----------------------------------------------------------------------
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug, Serialize, Deserialize, EnumString)]
+#[strum(serialize_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
+pub enum ExternalAuthProvider {
+    Github,
+    GenericOIDC,
+}
+
+#[derive(Clone, Entity)]
+pub struct ExternalAuthMap {
+    #[key]
+    pub external_user_id: String,
+    #[key]
+    pub provider: microrm::Serialized<ExternalAuthProvider>,
+
+    pub internal_user_id: UserID,
+}
+
+// ----------------------------------------------------------------------
+// Global container types
+// ----------------------------------------------------------------------
+
+#[derive(Clone, Default, Entity)]
+pub struct Realm {
+    #[key]
+    pub shortname: String,
+
+    pub clients: microrm::RelationMap<Client>,
+    pub groups: microrm::RelationMap<Group>,
+    pub keys: microrm::RelationMap<Key>,
+    pub roles: microrm::RelationMap<Role>,
+    pub scopes: microrm::RelationMap<Scope>,
+    pub users: microrm::RelationMap<User>,
+    pub auth_codes: microrm::RelationMap<AuthCode>,
+
+    pub external_auth: microrm::RelationMap<ExternalAuthMap>,
+    pub single_use_auth: microrm::RelationMap<SingleUseAuth>,
+}

+ 4 - 4
src/server/oidc.rs

@@ -29,7 +29,7 @@ pub enum OIDCErrorPayload<'a> {
     Owned(String),
 }
 
-impl<'a> OIDCErrorPayload<'a> {
+impl OIDCErrorPayload<'_> {
     fn as_str(&self) -> &str {
         match self {
             Self::Borrowed(s) => s,
@@ -44,7 +44,7 @@ impl<'a> From<&'a str> for OIDCErrorPayload<'a> {
     }
 }
 
-impl<'a> From<String> for OIDCErrorPayload<'a> {
+impl From<String> for OIDCErrorPayload<'_> {
     fn from(value: String) -> Self {
         Self::Owned(value)
     }
@@ -53,7 +53,7 @@ impl<'a> From<String> for OIDCErrorPayload<'a> {
 /// error type,
 pub struct OIDCError<'a>(OIDCErrorType, OIDCErrorPayload<'a>, Option<&'a str>);
 
-impl<'a> OIDCError<'a> {
+impl OIDCError<'_> {
     fn into_response(self) -> tide::Response {
         #[derive(Serialize)]
         struct ErrorOut<'a> {
@@ -74,7 +74,7 @@ impl<'a> OIDCError<'a> {
     }
 }
 
-impl<'a> From<microrm::Error> for OIDCError<'a> {
+impl From<microrm::Error> for OIDCError<'_> {
     fn from(value: microrm::Error) -> Self {
         Self(
             OIDCErrorType::ServerError,

+ 82 - 12
src/server/oidc/token.rs

@@ -1,5 +1,6 @@
 use super::{api, OIDCError, OIDCErrorType, Request};
 use crate::{
+    client::ClientExt,
     realm::RealmHelper,
     schema,
     server::session::SessionHelper,
@@ -255,39 +256,108 @@ pub(super) async fn do_token<'l>(mut request: Request) -> Result<tide::Response,
 
     let shelper = SessionHelper::new(&request);
     let mut lease = request.state().lease().await?;
+    let mut txn = lease.guard("do_token")?;
     let realm = shelper
-        .get_realm(&mut lease)
+        .get_realm(&mut txn)
         .map_err(|_| OIDCError(OIDCErrorType::InvalidRequest, "no such realm".into(), 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 basic_auth = request
+        .header(tide::http::headers::AUTHORIZATION)
+        .map(|auth| {
+            surf::http::auth::BasicAuth::from_credentials(auth.last().as_str()).map_err(|_| {
+                OIDCError(
+                    OIDCErrorType::InvalidRequest,
+                    "parsing http basic auth failed".into(),
+                    None,
+                )
+            })
+        });
+
+    let auth_client_name = if let Some(Err(e)) = basic_auth {
+        return Err(e);
+    } else if let Some(Ok(auth)) = &basic_auth {
+        // TODO: check if secret is correct
+        Some(auth.username())
+    } else {
+        None
+    };
+
+    let req_client_name = treq.client_id.as_ref();
+
+    let client_name = match (auth_client_name, req_client_name) {
+        (Some(aname), Some(rname)) => {
+            if aname == rname {
+                aname
+            } else {
+                return Err(OIDCError(
+                    OIDCErrorType::InvalidRequest,
+                    "different client names given in request and headers".into(),
+                    None,
+                ));
+            }
+        }
+        (Some(aname), None) => aname,
+        (None, Some(rname)) => rname,
+        (None, None) => {
+            return Err(OIDCError(
+                OIDCErrorType::InvalidRequest,
+                "no client given".into(),
+                None,
+            ))
+        }
+    };
+
     let client = realm
         .clients
         .keyed((realm.id(), client_name))
-        .get(&mut lease)?
+        .get(&mut txn)?
         .ok_or(OIDCError(
             OIDCErrorType::InvalidRequest,
             format!("unknown client name {client_name}").into(),
             None,
         ))?;
 
+    // check that the redirect_uri, if present, is valid
+    if let Some(redirect) = treq.redirect_uri.as_ref() {
+        match client.check_redirect(&mut txn, &redirect) {
+            Ok(true) => {}
+            Ok(false) => {
+                return Err(OIDCError(
+                    OIDCErrorType::InvalidRequest,
+                    "redirect_uri not valid for client".into(),
+                    None,
+                ))
+            }
+            Err(err) => {
+                log::warn!("failed to check redirect for client: {err}");
+                return Err(OIDCError(
+                    OIDCErrorType::ServerError,
+                    "could not check redirect".into(),
+                    None,
+                ));
+            }
+        }
+    }
+
     let rhelper = request
         .state()
         .core
         .realms
-        .get_helper(&mut lease, realm.id())
+        .get_helper(&mut txn, realm.id())
         .unwrap();
 
     if treq.grant_type == "authorization_code" {
-        do_authorization_code(&mut lease, &realm, rhelper.as_ref(), &client, &treq)
+        let r = do_authorization_code(&mut txn, &realm, rhelper.as_ref(), &client, &treq)?;
+        txn.commit()?;
+        Ok(r)
     } else if treq.grant_type == "refresh_token" {
-        do_refresh_token(&mut lease, &realm, rhelper.as_ref(), &client, &treq)
+        let r = do_refresh_token(&mut txn, &realm, rhelper.as_ref(), &client, &treq)?;
+        txn.commit()?;
+        Ok(r)
     } else if treq.grant_type == "password" {
-        do_direct_grant(&mut lease, &realm, rhelper.as_ref(), &client, &treq)
+        let r = do_direct_grant(&mut txn, &realm, rhelper.as_ref(), &client, &treq)?;
+        txn.commit()?;
+        Ok(r)
     } else {
         Err(OIDCError(
             OIDCErrorType::InvalidRequest,

+ 1 - 1
src/server/session.rs

@@ -126,7 +126,7 @@ impl<'l> SessionHelper<'l> {
     }
 }
 
-impl<'l> SessionHelper<'l> {
+impl SessionHelper<'_> {
     pub fn render_login_from_auth(
         &self,
         mut response: tide::Response,

+ 0 - 1
src/server/um.rs

@@ -180,5 +180,4 @@ pub(super) fn um_server(mut route: tide::Route<super::ServerStateWrapper>) {
         .get(|_req| async { Ok(tide::Redirect::permanent("um/")) });
     route.at("um/").get(um_index);
     route.at("um/update").post(um_update);
-    // route.at("/change_password").get(um_change_password).post(um_change_password_post);
 }