Skip to content

Commit

Permalink
Implement client authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
sugyan committed Aug 30, 2024
1 parent 3a9d084 commit 05911f7
Show file tree
Hide file tree
Showing 18 changed files with 706 additions and 179 deletions.
67 changes: 48 additions & 19 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ serde_json = "1.0.125"
# Cryptography
ecdsa = "0.16.9"
elliptic-curve = "0.13.6"
jose-jwa = "0.1.2"
jose-jwk = { version = "0.1.2", default-features = false }
k256 = { version = "0.13.3", default-features = false }
p256 = { version = "0.13.2", default-features = false }
rand = "0.8.5"
Expand All @@ -60,7 +62,7 @@ sha2 = "0.10.8"
# Networking
futures = { version = "0.3.30", default-features = false, features = ["alloc"] }
http = "1.1.0"
tokio = { version = "1.37", default-features = false }
tokio = { version = "1.39", default-features = false }

# HTTP client integrations
isahc = "1.7.2"
Expand Down
4 changes: 3 additions & 1 deletion atrium-oauth/oauth-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ keywords = ["atproto", "bluesky", "oauth"]
async-trait.workspace = true
atrium-api = { workspace = true, default-features = false }
atrium-xrpc.workspace = true
base64.workspace = true
chrono.workspace = true
ecdsa = { workspace = true, features = ["std", "signing", "verifying"] }
elliptic-curve = { workspace = true, features = ["jwk", "serde"] }
base64.workspace = true
jose-jwa.workspace = true
jose-jwk = { workspace = true, features = ["p256"] }
k256 = { workspace = true, features = ["ecdsa", "jwk", "serde"] }
p256 = { workspace = true, features = ["ecdsa", "jwk", "serde"] }
rand.workspace = true
Expand Down
97 changes: 62 additions & 35 deletions atrium-oauth/oauth-client/src/atproto.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::types::OAuthClientMetadata;
use crate::keyset::Keyset;
use crate::types::{OAuthClientMetadata, TryIntoOAuthClientMetadata};
use atrium_xrpc::http::Uri;
use thiserror::Error;

Expand All @@ -12,6 +13,10 @@ pub enum Error {
InvalidScope,
#[error("`redirect_uris` must not be empty")]
EmptyRedirectUris,
#[error("`private_key_jwt` auth method requires `jwks` keys")]
EmptyJwks,
#[error("`private_key_jwt` auth method requires `token_endpoint_auth_signing_alg`, otherwise must not be provided")]
AuthSigningAlg,
}

pub type Result<T> = core::result::Result<T, Error>;
Expand Down Expand Up @@ -50,14 +55,12 @@ impl From<GrantType> for String {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Scope {
Atproto,
OfflineAccess, // will be removed: https://github.com/bluesky-social/atproto/pull/2731/files#diff-8655bc89b9f05348fdc55d73ed4298ec3ff0edd03fcb601b30c514e61465ede7L207-L211
}

impl From<Scope> for String {
fn from(value: Scope) -> Self {
match value {
Scope::Atproto => String::from("atproto"),
Scope::OfflineAccess => String::from("offline_access"),
}
}
}
Expand All @@ -67,69 +70,93 @@ pub struct AtprotoLocalhostClientMetadata {
pub redirect_uris: Vec<String>,
}

impl TryFrom<AtprotoLocalhostClientMetadata> for OAuthClientMetadata {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtprotoClientMetadata {
pub client_id: String,
pub client_uri: String,
pub redirect_uris: Vec<String>,
pub token_endpoint_auth_method: AuthMethod,
pub grant_types: Vec<GrantType>,
pub scope: Vec<Scope>,
pub jwks_uri: Option<String>,
pub token_endpoint_auth_signing_alg: Option<String>,
}

impl TryIntoOAuthClientMetadata for AtprotoLocalhostClientMetadata {
type Error = Error;

fn try_from(value: AtprotoLocalhostClientMetadata) -> Result<Self> {
if value.redirect_uris.is_empty() {
fn try_into_client_metadata(self, _: &Option<Keyset>) -> Result<OAuthClientMetadata> {
if self.redirect_uris.is_empty() {
return Err(Error::EmptyRedirectUris);
}
Ok(OAuthClientMetadata {
client_id: String::from("http://localhost"),
client_uri: None,
redirect_uris: value.redirect_uris,
scope: Some(String::from("atproto")),
redirect_uris: self.redirect_uris,
scope: None, // will be set to `atproto`
grant_types: None, // will be set to `authorization_code` and `refresh_token`
token_endpoint_auth_method: None, // will be set to `none`
token_endpoint_auth_method: Some(String::from("none")),
dpop_bound_access_tokens: None, // will be set to `true`
jwks_uri: None,
jwks: None,
token_endpoint_auth_signing_alg: None,
})
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AtprotoClientMetadata {
pub client_id: String,
pub client_uri: String,
pub redirect_uris: Vec<String>,
pub token_endpoint_auth_method: AuthMethod,
pub grant_types: Vec<GrantType>,
pub scope: Vec<Scope>,
}

impl TryFrom<AtprotoClientMetadata> for OAuthClientMetadata {
impl TryIntoOAuthClientMetadata for AtprotoClientMetadata {
type Error = Error;

fn try_from(value: AtprotoClientMetadata) -> Result<Self> {
if value.client_id.parse::<Uri>().is_err() {
fn try_into_client_metadata(self, keyset: &Option<Keyset>) -> Result<OAuthClientMetadata> {
if self.client_id.parse::<Uri>().is_err() {
return Err(Error::InvalidClientId);
}
if value.redirect_uris.is_empty() {
if self.redirect_uris.is_empty() {
return Err(Error::EmptyRedirectUris);
}
if !value.grant_types.contains(&GrantType::AuthorizationCode) {
if !self.grant_types.contains(&GrantType::AuthorizationCode) {
return Err(Error::InvalidGrantTypes);
}
if !value.scope.contains(&Scope::Atproto) {
if !self.scope.contains(&Scope::Atproto) {
return Err(Error::InvalidScope);
}

// TODO: jwks

let (jwks_uri, mut jwks) = (self.jwks_uri, None);
match self.token_endpoint_auth_method {
AuthMethod::None => {
if self.token_endpoint_auth_signing_alg.is_some() {
return Err(Error::AuthSigningAlg);
}
}
AuthMethod::PrivateKeyJwt => {
if let Some(keyset) = keyset {
if self.token_endpoint_auth_signing_alg.is_none() {
return Err(Error::AuthSigningAlg);
}
if jwks_uri.is_none() {
jwks = Some(keyset.public_jwks());
}
} else {
return Err(Error::EmptyJwks);
}
}
}
Ok(OAuthClientMetadata {
client_id: value.client_id,
client_uri: Some(value.client_uri),
redirect_uris: value.redirect_uris,
token_endpoint_auth_method: Some(value.token_endpoint_auth_method.into()),
grant_types: Some(value.grant_types.into_iter().map(|v| v.into()).collect()),
client_id: self.client_id,
client_uri: Some(self.client_uri),
redirect_uris: self.redirect_uris,
token_endpoint_auth_method: Some(self.token_endpoint_auth_method.into()),
grant_types: Some(self.grant_types.into_iter().map(|v| v.into()).collect()),
scope: Some(
value
.scope
self.scope
.into_iter()
.map(|v| v.into())
.collect::<Vec<String>>()
.join(" "),
),
dpop_bound_access_tokens: Some(true),
jwks_uri,
jwks,
token_endpoint_auth_signing_alg: self.token_endpoint_auth_signing_alg,
})
}
}
2 changes: 2 additions & 0 deletions atrium-oauth/oauth-client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ pub enum Error {
#[error(transparent)]
ClientMetadata(#[from] crate::atproto::Error),
#[error(transparent)]
Keyset(#[from] crate::keyset::Error),
#[error(transparent)]
Resolver(#[from] crate::resolver::Error),
#[error(transparent)]
ServerAgent(#[from] crate::server_agent::Error),
Expand Down
28 changes: 28 additions & 0 deletions atrium-oauth/oauth-client/src/jose.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
pub mod jws;
pub mod jwt;
pub mod signing;

pub use self::signing::create_signed_jwt;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Header {
Jws(jws::Header),
// TODO: JWE?
}

#[cfg(test)]
mod tests {
use super::*;

// #[test]
// fn test_create_jwt() {
// let secret_key = SecretKey::<p256::NistP256>::from_slice(&[
// 178, 249, 128, 41, 213, 198, 33, 120, 72, 132, 129, 161, 128, 134, 36, 120, 199, 128,
// 234, 73, 217, 232, 94, 120, 78, 231, 64, 117, 105, 239, 160, 251,
// ])
// .expect("failed to create secret key");
// panic!("{:?}", create_jwt(&secret_key.into(), JwtClaims::default()));
// }
}
Loading

0 comments on commit 05911f7

Please sign in to comment.