Skip to content

Commit

Permalink
UMA2 - User managed access (#4)
Browse files Browse the repository at this point in the history
* Add uma2 discovery

* Got all the parameters for requesting an RPT in

* It compiles, yes?

* Before clippy

* Clippy is more or less happy

* Delete a UMA2 resource

* Search for UMA2 resources function

* Create UMA2 permission ticket

* Move UMA2 resources to their own file

* Create,update and delete associated permission for UMA2 resource

* Search for UMA2 resource permission

* Try to pair down interface changes

* Hide UMA2 behind a compile time flag

* Start of uma2 module, dividing up the UMA2 mega file

* UMA2 resource API calls into the resource module

* UMA2 Protection API moved to the permission_association module

* Rename uma2 module to rpt because it only has the rpt functionality in it

* UMA2 off by default

* Make sure that the build pipeline also build and tests the UMA2 code

* Add default empty vectors for OpenID Config JSONs that doesn't feature on the Keycloak UMA2 inspection

* Get the UMA2 config from the client

* Add Client Credentials Grant flow - RFC 6749 4.4

* No field like description, but display_name

* Resource owner and resource scope are a hash and not string when returned

* Implemented the permission association return values

* Fix used after move

* Have only one config struct

* Export auth method

* Permission ticket can be a list of things requested

* Searching for a resource returns a list of guids

* Get the permission ticket response

* Use UMA2 endpoint to interrogate OIDC when UMA2 is enabled

* Do not include OIDC discover in build for UMA2

* added DiscoveredUma2 ConfigUma2

* cargo fmt

Co-authored-by: Alexander Korolev <[email protected]>
  • Loading branch information
mvniekerk and kilork authored Jun 17, 2020
1 parent 6456a8f commit 9bba678
Show file tree
Hide file tree
Showing 21 changed files with 1,204 additions and 15 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ jobs:
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Build with UMA2
run: cargo build --features uma2
- name: Run tests with UMA2
run: cargo test --verbose --features uma2
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/target
**/*.rs.bk
Cargo.lock
*.iml
.idea/
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ license = 'Unlicense OR MIT'
readme = 'README.md'
repository = 'https://github.com/kilork/openid'

[features]
uma2 = []
default = []

[dependencies]
lazy_static = '1.4'
serde_json = '1'
Expand Down
1 change: 0 additions & 1 deletion src/claims.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use crate::Userinfo;
use base64;
use biscuit::SingleOrMultiple;
use url::Url;

Expand Down
41 changes: 32 additions & 9 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use crate::{
ClientError, Decode, Error, Expiry, Jose, Mismatch, Missing, Userinfo as ErrorUserinfo,
Validation,
},
Bearer, Claims, Config, Discovered, IdToken, OAuth2Error, Options, Provider, StandardClaims,
Token, Userinfo,
Bearer, Claims, Config, Configurable, Discovered, IdToken, OAuth2Error, Options, Provider,
StandardClaims, Token, Userinfo,
};
use biscuit::{
jwa::{self, SignatureAlgorithm},
Expand Down Expand Up @@ -62,7 +62,9 @@ impl<C: CompactJson + Claims> Client<Discovered, C> {
let http_client = reqwest::Client::new();
let config = discovered::discover(&http_client, issuer).await?;
let jwks = discovered::jwks(&http_client, config.jwks_uri.clone()).await?;
let provider = Discovered(config);

let provider = config.into();

Ok(Self::new(
provider,
id,
Expand All @@ -72,6 +74,9 @@ impl<C: CompactJson + Claims> Client<Discovered, C> {
Some(jwks),
))
}
}

impl<C: CompactJson + Claims, P: Provider + Configurable> Client<P, C> {
/// Passthrough to the redirect_url stored in inth_oauth2 as a str.
pub fn redirect_url(&self) -> &str {
self.redirect_uri
Expand All @@ -81,7 +86,7 @@ impl<C: CompactJson + Claims> Client<Discovered, C> {

/// A reference to the config document of the provider obtained via discovery
pub fn config(&self) -> &Config {
&self.provider.0
self.provider.config()
}

/// Constructs the auth_url to redirect a client to the provider. Options are... optional. Use
Expand All @@ -100,7 +105,7 @@ impl<C: CompactJson + Claims> Client<Discovered, C> {
None => String::from("openid"),
};

let mut url = self.auth_uri(Some(&scope), options.state.as_ref().map(String::as_str));
let mut url = self.auth_uri(Some(&scope), options.state.as_deref());
{
let mut query = url.query_pairs_mut();
if let Some(ref nonce) = options.nonce {
Expand Down Expand Up @@ -187,7 +192,7 @@ impl<C: CompactJson + Claims> Client<Discovered, C> {
};

if let Some(alg) = key.common.algorithm.as_ref() {
if let &jwa::Algorithm::Signature(sig) = alg {
if let jwa::Algorithm::Signature(sig) = *alg {
if header.registered.algorithm != sig {
return wrong_key!(sig, header.registered.algorithm);
}
Expand Down Expand Up @@ -277,7 +282,7 @@ impl<C: CompactJson + Claims> Client<Discovered, C> {
}
// By spec, if there are multiple auds, we must have an azp
if let SingleOrMultiple::Multiple(_) = claims.aud() {
if let None = claims.azp() {
if claims.azp().is_none() {
return Err(Validation::Missing(Missing::AuthorizedParty).into());
}
}
Expand Down Expand Up @@ -491,6 +496,25 @@ where
Ok(token)
}

/// Requests an access token using the Client Credentials Grant flow
///
/// See [RFC 6749, section 4.4](https://tools.ietf.org/html/rfc6749#section-4.4)
pub async fn request_token_using_client_credentials(&self) -> Result<Bearer, ClientError> {
// Ensure the non thread-safe `Serializer` is not kept across
// an `await` boundary by localizing it to this inner scope.
let body = {
let mut body = Serializer::new(String::new());
body.append_pair("grant_type", "client_credentials");
body.append_pair("client_id", &self.client_id);
body.append_pair("client_secret", &self.client_secret);
body.finish()
};

let json = self.post_token(body).await?;
let token: Bearer = serde_json::from_value(json)?;
Ok(token)
}

/// Refreshes an access token.
///
/// See [RFC 6749, section 6](http://tools.ietf.org/html/rfc6749#section-6).
Expand All @@ -505,8 +529,7 @@ where
"refresh_token",
token
.refresh_token
.as_ref()
.map(String::as_str)
.as_deref()
.expect("No refresh_token field"),
);

Expand Down
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ pub struct Config {
#[serde(default)]
pub acr_values_supported: Option<Vec<String>>,
// pairwise and public are valid by spec, but servers can add more
#[serde(default = "empty_string_vec")]
pub subject_types_supported: Vec<String>,
// Must include at least RS256, none is only allowed with response types without id tokens
#[serde(default = "empty_string_vec")]
pub id_token_signing_alg_values_supported: Vec<String>,
#[serde(default)]
pub id_token_encryption_alg_values_supported: Option<Vec<String>>,
Expand Down Expand Up @@ -93,3 +95,7 @@ pub struct Config {
fn tru() -> bool {
true
}

fn empty_string_vec() -> Vec<String> {
vec![]
}
5 changes: 5 additions & 0 deletions src/configurable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use crate::Config;

pub trait Configurable {
fn config(&self) -> &Config;
}
17 changes: 15 additions & 2 deletions src/discovered.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::{error::Error, Config, Provider};
use crate::{error::Error, Config, Configurable, Provider};
use biscuit::jwk::JWKSet;
use biscuit::Empty;
use reqwest::Client;
use url::Url;

pub struct Discovered(pub Config);
pub struct Discovered(Config);

impl Provider for Discovered {
fn auth_uri(&self) -> &Url {
Expand All @@ -16,11 +16,24 @@ impl Provider for Discovered {
}
}

impl Configurable for Discovered {
fn config(&self) -> &Config {
&self.0
}
}

impl From<Config> for Discovered {
fn from(value: Config) -> Self {
Self(value)
}
}

pub async fn discover(client: &Client, mut issuer: Url) -> Result<Config, Error> {
issuer
.path_segments_mut()
.map_err(|_| Error::CannotBeABase)?
.extend(&[".well-known", "openid-configuration"]);

let resp = client.get(issuer).send().await?;
resp.json().await.map_err(Error::from)
}
Expand Down
12 changes: 12 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ pub enum ClientError {

/// OAuth 2.0 error.
OAuth2(OAuth2Error),

/// UMA2 error.
#[cfg(feature = "uma2")]
Uma2(Uma2Error),
}

impl fmt::Display for ClientError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
Expand All @@ -113,6 +118,8 @@ impl fmt::Display for ClientError {
ClientError::Json(ref err) => write!(f, "{}", err),
// ClientError::Parse(ref err) => write!(f, "{}", err),
ClientError::OAuth2(ref err) => write!(f, "{}", err),
#[cfg(feature = "uma2")]
ClientError::Uma2(ref err) => write!(f, "{}", err),
}
}
}
Expand All @@ -126,6 +133,8 @@ impl std::error::Error for ClientError {
ClientError::Json(ref err) => Some(err),
// ClientError::Parse(ref err) => Some(err),
ClientError::OAuth2(ref err) => Some(err),
#[cfg(feature = "uma2")]
ClientError::Uma2(ref err) => Some(err),
}
}
}
Expand Down Expand Up @@ -154,6 +163,9 @@ pub use serde_json::Error as Json;

use failure::Fail;

#[cfg(feature = "uma2")]
use crate::uma2::Uma2Error;

#[derive(Debug, Fail)]
pub enum Error {
#[fail(display = "{}", _0)]
Expand Down
9 changes: 8 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ mod bearer;
mod claims;
mod client;
mod config;
mod configurable;
mod custom_claims;
mod discovered;
mod display;
Expand All @@ -335,13 +336,17 @@ mod standard_claims;
mod token;
mod userinfo;

#[cfg(feature = "uma2")]
pub mod uma2;

pub use ::biscuit::jws::Compact as Jws;
pub use ::biscuit::{Compact, CompactJson, Empty, SingleOrMultiple};
pub use address::Address;
pub use bearer::Bearer;
pub use claims::Claims;
pub use client::Client;
pub use config::Config;
pub use configurable::Configurable;
pub use custom_claims::CustomClaims;
pub use discovered::Discovered;
pub use display::Display;
Expand All @@ -353,10 +358,12 @@ pub use standard_claims::StandardClaims;
pub use token::Token;
pub use userinfo::Userinfo;

/// Reimport `biscuit` depdendency.
/// Reimport `biscuit` dependency.
pub mod biscuit {
pub use biscuit::*;
}

type IdToken<T> = Jws<T, Empty>;
pub type DiscoveredClient = Client<Discovered, StandardClaims>;
#[cfg(feature = "uma2")]
pub type DiscoveredUma2Client = Client<uma2::DiscoveredUma2, StandardClaims>;
4 changes: 2 additions & 2 deletions src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ pub mod google {
/// See [Choosing a redirect URI][uri].
///
/// [uri]: https://developers.google.com/identity/protocols/OAuth2InstalledApp#choosingredirecturi
pub const REDIRECT_URI_OOB: &'static str = "urn:ietf:wg:oauth:2.0:oob";
pub const REDIRECT_URI_OOB: &str = "urn:ietf:wg:oauth:2.0:oob";

/// Signals the server to return the authorization code in the page title.
///
/// See [Choosing a redirect URI][uri].
///
/// [uri]: https://developers.google.com/identity/protocols/OAuth2InstalledApp#choosingredirecturi
pub const REDIRECT_URI_OOB_AUTO: &'static str = "urn:ietf:wg:oauth:2.0:oob:auto";
pub const REDIRECT_URI_OOB_AUTO: &str = "urn:ietf:wg:oauth:2.0:oob:auto";

lazy_static! {
static ref AUTH_URI: Url =
Expand Down
23 changes: 23 additions & 0 deletions src/uma2/claim_token_format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use core::fmt;
use serde::export::Formatter;

/// UMA2 claim token format
/// Either is an access token (urn:ietf:params:oauth:token-type:jwt) or an OIDC ID token
pub enum Uma2ClaimTokenFormat {
OAuthJwt, // urn:ietf:params:oauth:token-type:jwt
OidcIdToken, // https://openid.net/specs/openid-connect-core-1_0.html#IDToken
}

impl fmt::Display for Uma2ClaimTokenFormat {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match *self {
Uma2ClaimTokenFormat::OAuthJwt => "urn:ietf:params:oauth:token-type:jwt",
Uma2ClaimTokenFormat::OidcIdToken =>
"https://openid.net/specs/openid-connect-core-1_0.html#IDToken",
}
)
}
}
19 changes: 19 additions & 0 deletions src/uma2/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use crate::Config;
use serde::{Deserialize, Serialize};
use url::Url;

#[derive(Debug, Deserialize, Serialize)]
pub struct Uma2Config {
// UMA2 additions
#[serde(default)]
pub resource_registration_endpoint: Option<Url>,
#[serde(default)]
pub permission_endpoint: Option<Url>,
#[serde(default)]
pub policy_endpoint: Option<Url>,
#[serde(default)]
pub introspection_endpoint: Option<Url>,

#[serde(flatten)]
pub config: Config,
}
Loading

0 comments on commit 9bba678

Please sign in to comment.