Skip to content

Commit

Permalink
fixed #406 - Apple ID SSO
Browse files Browse the repository at this point in the history
  • Loading branch information
Eugeny committed Nov 21, 2022
1 parent 5a7c39c commit fffd799
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 43 deletions.
54 changes: 30 additions & 24 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion warpgate-sso/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ bytes = "1.2"
thiserror = "1.0"
tokio = { version = "1.20", features = ["tracing", "macros"] }
tracing = "0.1"
openidconnect = { version = "2.4", features = ["reqwest", "rustls-tls"] }
openidconnect = { version = "2.4", features = ["reqwest", "rustls-tls", "accept-string-booleans"] }
serde = "1.0"
serde_json = "1.0"
once_cell = "1.14"
jsonwebtoken = "8"
data-encoding = "2.3"
92 changes: 83 additions & 9 deletions warpgate-sso/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::collections::HashMap;
use std::time::SystemTime;

use data_encoding::BASE64;
use once_cell::sync::Lazy;
use openidconnect::{ClientId, ClientSecret, IssuerUrl};
use openidconnect::{AuthType, ClientId, ClientSecret, IssuerUrl};
use serde::{Deserialize, Serialize};

use crate::SsoError;
Expand Down Expand Up @@ -42,6 +44,8 @@ pub enum SsoInternalProviderConfig {
Apple {
client_id: ClientId,
client_secret: ClientSecret,
key_id: String,
team_id: String,
},
#[serde(rename = "azure")]
Azure {
Expand All @@ -58,6 +62,15 @@ pub enum SsoInternalProviderConfig {
},
}

#[derive(Debug, Serialize)]
struct AppleIDClaims<'a> {
sub: &'a str,
aud: &'a str,
exp: usize,
nbf: usize,
iss: &'a str,
}

impl SsoInternalProviderConfig {
#[inline]
pub fn label(&self) -> &'static str {
Expand All @@ -80,13 +93,53 @@ impl SsoInternalProviderConfig {
}

#[inline]
pub fn client_secret(&self) -> &ClientSecret {
match self {
pub fn client_secret(&self) -> Result<ClientSecret, SsoError> {
Ok(match self {
SsoInternalProviderConfig::Google { client_secret, .. }
| SsoInternalProviderConfig::Apple { client_secret, .. }
| SsoInternalProviderConfig::Azure { client_secret, .. }
| SsoInternalProviderConfig::Custom { client_secret, .. } => client_secret,
}
| SsoInternalProviderConfig::Custom { client_secret, .. } => client_secret.clone(),
SsoInternalProviderConfig::Apple {
client_secret,
client_id,
key_id,
team_id,
} => {
let key_content =
BASE64
.decode(client_secret.secret().as_bytes())
.map_err(|e| {
SsoError::ConfigError(format!(
"could not decode base64 client_secret: {e}"
))
})?;
let key = jsonwebtoken::EncodingKey::from_ec_pem(&key_content).map_err(|e| {
SsoError::ConfigError(format!(
"could not parse client_secret as a private key: {e}"
))
})?;
let mut header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::ES256);
header.kid = Some(key_id.into());

ClientSecret::new(jsonwebtoken::encode(
&header,
&AppleIDClaims {
aud: &APPLE_ISSUER_URL,
sub: client_id,
exp: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as usize
+ 600,
nbf: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs() as usize,
iss: team_id,
},
&key,
)?)
}
})
}

#[inline]
Expand All @@ -104,10 +157,11 @@ impl SsoInternalProviderConfig {
#[inline]
pub fn scopes(&self) -> Vec<String> {
match self {
SsoInternalProviderConfig::Google { .. }
| SsoInternalProviderConfig::Apple { .. }
| SsoInternalProviderConfig::Azure { .. } => vec!["email".to_string()],
SsoInternalProviderConfig::Google { .. } | SsoInternalProviderConfig::Azure { .. } => {
vec!["email".to_string()]
}
SsoInternalProviderConfig::Custom { scopes, .. } => scopes.clone(),
SsoInternalProviderConfig::Apple { .. } => vec![],
}
}

Expand All @@ -124,4 +178,24 @@ impl SsoInternalProviderConfig {
}
}
}

#[inline]
pub fn auth_type(&self) -> AuthType {
match self {
SsoInternalProviderConfig::Google { .. }
| SsoInternalProviderConfig::Custom { .. }
| SsoInternalProviderConfig::Azure { .. } => AuthType::BasicAuth,
SsoInternalProviderConfig::Apple { .. } => AuthType::RequestBody,
}
}

#[inline]
pub fn needs_pkce_verifier(&self) -> bool {
match self {
SsoInternalProviderConfig::Google { .. }
| SsoInternalProviderConfig::Custom { .. }
| SsoInternalProviderConfig::Azure { .. } => true,
SsoInternalProviderConfig::Apple { .. } => false,
}
}
}
4 changes: 4 additions & 0 deletions warpgate-sso/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub enum SsoError {
Mitm,
#[error("config parse error: {0}")]
UrlParse(#[from] openidconnect::url::ParseError),
#[error("config error: {0}")]
ConfigError(String),
#[error("provider discovery error: {0}")]
Discovery(String),
#[error("code verification error: {0}")]
Expand All @@ -20,6 +22,8 @@ pub enum SsoError {
Signing(#[from] SigningError),
#[error("I/O: {0}")]
Io(#[from] std::io::Error),
#[error("JWT error: {0}")]
Jwt(#[from] jsonwebtoken::errors::Error),
#[error(transparent)]
Other(Box<dyn Error + Send + Sync>),
}
16 changes: 12 additions & 4 deletions warpgate-sso/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct SsoLoginRequest {
pub(crate) csrf_token: CsrfToken,
pub(crate) nonce: Nonce,
pub(crate) redirect_url: RedirectUrl,
pub(crate) pkce_verifier: PkceCodeVerifier,
pub(crate) pkce_verifier: Option<PkceCodeVerifier>,
pub(crate) config: SsoInternalProviderConfig,
}

Expand All @@ -32,15 +32,23 @@ impl SsoLoginRequest {
.await?
.set_redirect_uri(self.redirect_url.clone());

let token_response = client
.exchange_code(AuthorizationCode::new(code))
.set_pkce_verifier(self.pkce_verifier)
let mut req = client.exchange_code(AuthorizationCode::new(code));
if let Some(verifier) = self.pkce_verifier {
req = req.set_pkce_verifier(verifier);
}

let token_response = req
.request_async(async_http_client)
.await
.map_err(|e| match e {
RequestTokenError::ServerResponse(response) => {
SsoError::Verification(response.error().to_string())
}
RequestTokenError::Parse(err, path) => SsoError::Verification(format!(
"Parse error: {:?} / {:?}",
err,
String::from_utf8_lossy(&path)
)),
e => SsoError::Verification(format!("{e}")),
})?;

Expand Down
17 changes: 12 additions & 5 deletions warpgate-sso/src/sso.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ pub async fn make_client(config: &SsoInternalProviderConfig) -> Result<CoreClien
Ok(CoreClient::from_provider_metadata(
metadata,
config.client_id().clone(),
Some(config.client_secret().clone()),
))
Some(config.client_secret()?),
)
.set_auth_type(config.auth_type()))
}

impl SsoClient {
Expand All @@ -34,8 +35,6 @@ impl SsoClient {
}

pub async fn start_login(&self, redirect_url: String) -> Result<SsoLoginRequest, SsoError> {
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();

let redirect_url = RedirectUrl::new(redirect_url)?;
let client = make_client(&self.config).await?;
let mut auth_req = client
Expand All @@ -54,7 +53,15 @@ impl SsoClient {
auth_req = auth_req.add_scope(Scope::new(scope.to_string()));
}

let (auth_url, csrf_token, nonce) = auth_req.set_pkce_challenge(pkce_challenge).url();
let pkce_verifier = if self.config.needs_pkce_verifier() {
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
auth_req = auth_req.set_pkce_challenge(pkce_challenge);
Some(pkce_verifier)
} else {
None
};

let (auth_url, csrf_token, nonce) = auth_req.url();

Ok(SsoLoginRequest {
auth_url,
Expand Down

0 comments on commit fffd799

Please sign in to comment.