-
-
Notifications
You must be signed in to change notification settings - Fork 16
Single sign-on (OAuth/OIDC) #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c370344
d2b099a
b6b0dd5
5abc6cb
c028411
7a462a7
eac6111
e97a075
1062f9c
00bdd97
6d02a5e
d61b9b0
adbc568
7aae4b6
5a25820
1d7b41c
d3b5e6f
0240d4e
6c9d53c
73a3c42
cf1dedf
aa34f7e
815cc86
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| server_url = "https://inv.kurosaki.cx" | ||
|
|
||
| [sso.gitlab] | ||
| issuer = "https://gitlab.computer.surgery" | ||
| scopes = ["openid", "email"] | ||
| code_challenge = false | ||
|
|
||
| [sso.gitlab.endpoints] | ||
| type = "discoverable" | ||
|
|
||
| [sso.gitlab.credentials] | ||
| type = "post" | ||
| client_id = "xxx" | ||
| client_secret = "xxx" | ||
|
|
||
| [sso.gitlab.claims] |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,274 @@ | ||||||
| use std::{ | ||||||
| borrow::Borrow, | ||||||
| collections::{HashMap, HashSet}, | ||||||
| hash::{Hash, Hasher}, | ||||||
| ops::Deref, | ||||||
| str::FromStr, | ||||||
| }; | ||||||
|
|
||||||
| use reqwest::Url; | ||||||
| use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; | ||||||
|
|
||||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||||
| #[serde(rename_all = "lowercase", tag = "type")] | ||||||
| pub enum Endpoints { | ||||||
| Discoverable, | ||||||
| Manual { | ||||||
| authorization: Box<Url>, | ||||||
| token: Box<Url>, | ||||||
| userinfo: Box<Url>, | ||||||
| }, | ||||||
| } | ||||||
|
|
||||||
| #[derive(Debug, Serialize, Deserialize, Clone)] | ||||||
| #[serde(rename_all = "lowercase", tag = "type")] | ||||||
| pub enum Credentials { | ||||||
| None { | ||||||
| client_id: String, | ||||||
| }, | ||||||
| Basic { | ||||||
| client_id: String, | ||||||
| client_secret: String, | ||||||
| }, | ||||||
| Post { | ||||||
| client_id: String, | ||||||
| client_secret: String, | ||||||
| }, | ||||||
| } | ||||||
|
|
||||||
| impl Credentials { | ||||||
| pub fn client_id(&self) -> &str { | ||||||
| match self { | ||||||
| Credentials::None { client_id } | ||||||
| | Credentials::Basic { client_id, .. } | ||||||
| | Credentials::Post { client_id, .. } => client_id, | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| #[derive(Debug, Serialize, Clone, PartialEq, Eq, Hash)] | ||||||
| #[serde(rename_all = "lowercase")] | ||||||
| pub enum Claim { | ||||||
| Id, | ||||||
| Username, | ||||||
| Picture, | ||||||
| Email, | ||||||
| Custom(String), | ||||||
| } | ||||||
|
|
||||||
| #[derive(Clone, Debug)] | ||||||
| pub struct IdProvider { | ||||||
| pub id: String, | ||||||
|
|
||||||
| pub issuer: Url, | ||||||
| pub name: Option<String>, | ||||||
| pub icon: Option<Url>, | ||||||
|
|
||||||
| pub scopes: Vec<String>, | ||||||
| pub endpoints: Endpoints, | ||||||
| pub credentials: Credentials, | ||||||
| pub claims: HashMap<Claim, String>, | ||||||
|
|
||||||
| pub code_challenge: bool, | ||||||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| } | ||||||
|
|
||||||
| impl Borrow<str> for IdProvider { | ||||||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The following impls are defined in order to prevent ID providers with duplicate IDs in the configuration. |
||||||
| fn borrow(&self) -> &str { | ||||||
| &self.id | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl PartialEq for IdProvider { | ||||||
| fn eq(&self, other: &Self) -> bool { | ||||||
| self.id == other.id | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl Eq for IdProvider {} | ||||||
|
|
||||||
| impl Hash for IdProvider { | ||||||
| fn hash<H>(&self, state: &mut H) | ||||||
| where | ||||||
| H: Hasher, | ||||||
| { | ||||||
| self.id.hash(state); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| #[derive(Debug, Default, Clone, PartialEq, Eq)] | ||||||
| pub struct SSO(HashSet<IdProvider>); | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason this isnt a
Suggested change
|
||||||
|
|
||||||
| impl Serialize for SSO { | ||||||
| fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||||||
| where | ||||||
| S: Serializer, | ||||||
| { | ||||||
| #[derive(Serialize)] | ||||||
| struct Mock { | ||||||
| pub issuer: Url, | ||||||
| pub name: Option<String>, | ||||||
| pub icon: Option<Url>, | ||||||
| pub scopes: Vec<String>, | ||||||
| pub endpoints: Endpoints, | ||||||
| pub credentials: Credentials, | ||||||
| pub claims: HashMap<Claim, String>, | ||||||
| pub code_challenge: bool, | ||||||
| } | ||||||
|
|
||||||
| let map: HashMap<String, Mock> = self | ||||||
| .iter() | ||||||
| .map(|provider| { | ||||||
| ( | ||||||
| provider.id.clone(), | ||||||
| Mock { | ||||||
| issuer: provider.issuer.clone(), | ||||||
| name: provider.name.clone(), | ||||||
| icon: provider.icon.clone(), | ||||||
| scopes: provider.scopes.clone(), | ||||||
| endpoints: provider.endpoints.clone(), | ||||||
| credentials: provider.credentials.clone(), | ||||||
| claims: provider.claims.clone(), | ||||||
| code_challenge: provider.code_challenge, | ||||||
| }, | ||||||
| ) | ||||||
| }) | ||||||
| .collect(); | ||||||
|
|
||||||
| map.serialize(serializer) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl<'de> Deserialize<'de> for SSO { | ||||||
| fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||||||
| where | ||||||
| D: Deserializer<'de>, | ||||||
| { | ||||||
| #[derive(Deserialize)] | ||||||
| pub struct Mock { | ||||||
| pub issuer: Url, | ||||||
| pub name: Option<String>, | ||||||
| pub icon: Option<Url>, | ||||||
|
|
||||||
| pub scopes: Vec<String>, | ||||||
| pub endpoints: Endpoints, | ||||||
| pub credentials: Credentials, | ||||||
| pub claims: HashMap<Claim, String>, | ||||||
|
|
||||||
| pub code_challenge: bool, | ||||||
| } | ||||||
|
|
||||||
| let map: HashMap<String, Mock> = | ||||||
| HashMap::deserialize(deserializer).map_err(de::Error::custom)?; | ||||||
|
|
||||||
| Ok(SSO(map | ||||||
| .into_iter() | ||||||
| .map(|(id, mock)| IdProvider { | ||||||
| id, | ||||||
| issuer: mock.issuer, | ||||||
| name: mock.name, | ||||||
| icon: mock.icon, | ||||||
| scopes: mock.scopes, | ||||||
| endpoints: mock.endpoints, | ||||||
| credentials: mock.credentials, | ||||||
| claims: mock.claims, | ||||||
| code_challenge: mock.code_challenge, | ||||||
| }) | ||||||
| .collect())) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl Deref for SSO { | ||||||
| type Target = HashSet<IdProvider>; | ||||||
|
|
||||||
| fn deref(&self) -> &Self::Target { | ||||||
| &self.0 | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl<'de> Deserialize<'de> for Claim { | ||||||
| fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||||||
| where | ||||||
| D: Deserializer<'de>, | ||||||
| { | ||||||
| Deserialize::deserialize(deserializer) | ||||||
| .and_then(|s| str::parse(s).map_err(de::Error::custom)) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| impl FromStr for Claim { | ||||||
| type Err = std::convert::Infallible; | ||||||
|
|
||||||
| fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||||
| Ok(match s.to_lowercase().as_str() { | ||||||
| "sub" | "id" => Claim::Id, | ||||||
| "username" | "preferred_username" => Claim::Username, | ||||||
| "picture" => Claim::Picture, | ||||||
| "email" => Claim::Email, | ||||||
| other => Claim::Custom(other.to_string()), | ||||||
| }) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| #[cfg(test)] | ||||||
| mod tests { | ||||||
| use super::*; | ||||||
|
|
||||||
| #[test] | ||||||
| fn deserialize_sso_config() { | ||||||
| let value = serde_json::json!( | ||||||
| { | ||||||
| "Gitlab": { | ||||||
| "issuer": "https://gitlab.com", | ||||||
| "scopes": ["openid"], | ||||||
|
|
||||||
| "endpoints": { | ||||||
| "type": "discoverable" | ||||||
| }, | ||||||
| "credentials": { | ||||||
| "type": "post", | ||||||
| "client_id": "foobar", | ||||||
| "client_secret": "baz" | ||||||
| }, | ||||||
| "claims": { | ||||||
| "id": "sub", | ||||||
| "email": "preferred_email" | ||||||
| }, | ||||||
|
|
||||||
| "code_challenge": false, | ||||||
| } | ||||||
| } | ||||||
| ); | ||||||
|
|
||||||
| let result: SSO = serde_json::from_value(value).expect("config deserializes successfully"); | ||||||
|
|
||||||
| assert_eq!( | ||||||
| result, | ||||||
| SSO([IdProvider { | ||||||
| id: "Gitlab".to_owned(), | ||||||
|
|
||||||
| issuer: "https://gitlab.com" | ||||||
| .parse() | ||||||
| .expect("issuer should be valid"), | ||||||
| name: None, | ||||||
| icon: None, | ||||||
|
|
||||||
| scopes: vec!["openid".to_owned()], | ||||||
| endpoints: Endpoints::Discoverable, | ||||||
| credentials: Credentials::Post { | ||||||
| client_id: "foobar".to_owned(), | ||||||
| client_secret: "baz".to_owned(), | ||||||
| }, | ||||||
| claims: [ | ||||||
| (Claim::Id, "sub".to_owned()), | ||||||
| (Claim::Email, "preferred_email".to_owned()) | ||||||
| ] | ||||||
| .into_iter() | ||||||
| .collect(), | ||||||
|
|
||||||
| code_challenge: false, | ||||||
| }] | ||||||
| .into_iter() | ||||||
| .collect()) | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This allows flexibility for claims, claims are a hashmap of information about the user, returned in the token or userinfo response i.e. ("preferred_email" -> "[email protected]", etc.) and this will allow cases where we wanna point out that "preferred_email" corresponds to the
Emailclaim in case of unconventional keys.