Skip to content

Commit

Permalink
Set audience validation default to off (#25)
Browse files Browse the repository at this point in the history
* Default turn off finicky default audience validation

* Update changelog

* Start fixing no userinfo

* Fix example using pkce

* Add back basic example

* Bump major version, update changelog

* Lint

* Prep release
  • Loading branch information
MarcusGrass authored Dec 19, 2023
1 parent 8371a2e commit 9ecded1
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 30 deletions.
15 changes: 13 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

<!-- next-header -->
## [Unreleased] - ReleaseDate
## Changed
## [x.x.x] - Unreleased

## [0.7.0] - 2023-12-19
### Changed
- Change default behaviour back since updating `jsonwebtoken` to `0.9x` to client-based audience validation
instead of library audience-validation. IE. The user validates their own `aud`, if wanted.
- Make userinfo-endpoint on `Provider` optional as it's `RECOMMENDED` according to the oidc-spec.

### Fixed
- Fix a bug where secret wasn't passed through if using the `PKCE`-flow with a client-secret

## [0.6.1] - 2023-10-25
### Changed
- [PR#23](https://github.com/EmbarkStudios/tame-oidc/pull/23) replaced `base64` with `data-encoding`
- [PR#24](https://github.com/EmbarkStudios/tame-oidc/pull/24) upgraded `ring` from 0.16 -> 0.17.

Expand Down
20 changes: 13 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "tame-oidc"
description = "A (very) thin layer of OIDC like functionality"
version = "0.5.0"
version = "0.7.0"
authors = [
"Embark <[email protected]>",
"Mathias Tervo <[email protected]>",
Expand All @@ -18,27 +18,33 @@ doctest = false
path = "src/lib.rs"

[[example]]
name = "embark"
path = "examples/embark.rs"
name = "embark-pkce"
path = "examples/embark-pkce.rs"

[[example]]
name = "embark-basic"
path = "examples/embark-basic.rs"

[dependencies]
data-encoding = "2.4"
http = "0.2"
jsonwebtoken = "9.1"
jsonwebtoken = "9.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
url = "2.3"
thiserror = "1"

## dev dependencies below
[dev-dependencies]
bytes = "1.2"
bytes = "1.4"
rand = "0.8.5"
ring = "0.17.7"

[dev-dependencies.reqwest]
version = "0.11"
version = "0.11.22"
features = ["rustls-tls"]
default-features = false

[dev-dependencies.tokio]
version = "1.21"
version = "1.35.0"
features = ["macros", "rt-multi-thread"]
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,20 @@ See example code in `examples/embark.rs`

## Examples

### [embark](examples/embark.rs)
### [embark basic](examples/embark-basic)

Usage: `cargo run --example embark`
Usage: `cargo run --example embark-basic`

A small example of using `tame-oidc` together with [reqwest](https://github.com/seanmonstar/reqwest).
A small example of using `tame-oidc` together with [reqwest](https://github.com/seanmonstar/reqwest) using
the basic auth flow.


### [embark pkce](examples/embark-pkce)

Usage: `cargo run --example embark-pkce`

A small example of using `tame-oidc` together with [reqwest](https://github.com/seanmonstar/reqwest) using the PKCE
auth flow.

## Contributing

Expand Down
32 changes: 23 additions & 9 deletions examples/embark.rs → examples/embark-basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ fn handle_connection(mut stream: TcpStream) -> Option<String> {

/// Spins up a listener on port, waits for any request from
/// the authentication provider and tries to return an `auth_code`
async fn listener(redirect_uri: &str) -> String {
let listener = TcpListener::bind(redirect_uri).unwrap();
println!("Listening on {}", redirect_uri);
async fn listener(host: &str, port: u16) -> String {
let urn = format!("{host}:{port}");
let listener = TcpListener::bind(&urn).unwrap();
println!("Listening on {}", urn);

let mut auth_code = String::new();
for stream in listener.incoming() {
Expand Down Expand Up @@ -76,30 +77,43 @@ async fn http_send<Body: Into<reqwest::Body>>(
#[tokio::main]
async fn main() {
let http_client = reqwest::Client::new();

let issuer_domain = std::env::var("ISSUER_DOMAIN").unwrap();
// Secret is optional in the PKCE flow
let client_secret = std::env::var("CLIENT_SECRET").unwrap();
let client_id = std::env::var("CLIENT_ID").unwrap();
let redirect_uri = "127.0.0.1:8000";
let host = "127.0.0.1";
let port = 8000u16;
// It's very important that this exactly matches where it's provided in other places, protocol and trailing slash all
let redirect_uri = format!("http://{host}:{port}/");

// Fetch and instantiate a provider using a `well-known` uri from an issuer
let request = provider::well_known(&issuer_domain).unwrap();
let response = http_send(&http_client, request).await;
let provider = Provider::from_response(response).unwrap();

let auth_endpoint = provider.authorization_endpoint.to_string();
// 1. Authenticate through web browser
// user goes to embark auth url in browser
// auth service returns auth_code to listener at `redirect_uri`
let auth_code = listener(redirect_uri).await;
// Add idp-specific extra query-parameters to the below `authorize_url`
let authorize_url = format!(
"{auth_endpoint}?\
response_type=code&\
client_id={client_id}&\
redirect_uri={redirect_uri}&\
scope=openid+offline"
);
println!("Authorize at {authorize_url}");

let auth_code = listener(host, port).await;
println!("Listener closed down");
println!("Final code {}", auth_code);

// 3. User now has 2 minutes to swap the auth code for an Embark Access token.
// Make a `POST` request to the auth service /oauth2/token
let scheme = AuthenticationScheme::Basic(ClientCredentials::new(client_secret));
let scheme = AuthenticationScheme::Basic(ClientCredentials::new(client_secret.clone()));
let client_authentication = ClientAuthentication::new(client_id, scheme, None, None);
let exchange_request = provider
.exchange_token_request(redirect_uri, &client_authentication, &auth_code)
.exchange_token_request(&redirect_uri, &client_authentication, &auth_code)
.unwrap();

let response = http_send(&http_client, exchange_request).await;
Expand Down
163 changes: 163 additions & 0 deletions examples/embark-pkce.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#![allow(clippy::dbg_macro)]

use bytes::Bytes;
use http::Request;
use rand::rngs::ThreadRng;
use rand::RngCore;
use reqwest::Url;
use std::{
convert::TryInto,
io::{prelude::*, BufReader},
net::{TcpListener, TcpStream},
str,
};
use tame_oidc::auth_scheme::{AuthenticationScheme, ClientAuthentication, PkceCredentials};
use tame_oidc::provider::Claims;
use tame_oidc::{
oidc::Token,
provider::{self, Provider, JWKS},
};

fn http_status_ok() -> String {
"HTTP/1.1 200 OK\r\n\r\n".to_string()
}

fn handle_connection(mut stream: TcpStream) -> Option<String> {
let mut reader = BufReader::new(&stream);
let mut request = String::new();
reader.read_line(&mut request).unwrap();

let query_params = request.split_whitespace().nth(1).unwrap();
let url = Url::parse(&format!("http://127.0.0.1:8000{query_params}")).unwrap();

stream.write_all(http_status_ok().as_bytes()).unwrap();
stream.flush().unwrap();

// Extract the `code` query param and value
url.query_pairs()
.find(|(key, _)| key == "code")
.map(|(_, code)| code.to_string())
}

/// Spins up a listener on port, waits for any request from
/// the authentication provider and tries to return an `auth_code`
async fn listener(host: &str, port: u16) -> String {
let urn = format!("{host}:{port}");
let listener = TcpListener::bind(&urn).unwrap();
println!("Listening on {}", urn);

let mut auth_code = String::new();
for stream in listener.incoming() {
let stream = stream.unwrap();
if let Some(code) = handle_connection(stream) {
auth_code = code;
break;
};
}

auth_code.trim().to_string()
}

/// Return reqwest response
async fn http_send<Body: Into<reqwest::Body>>(
http_client: &reqwest::Client,
request: Request<Body>,
) -> http::Response<Bytes> {
// Make the request
let mut response = http_client
.execute(request.try_into().unwrap())
.await
.unwrap();
// Convert to http::Response
let mut builder = http::Response::builder()
.status(response.status())
.version(response.version());
std::mem::swap(builder.headers_mut().unwrap(), response.headers_mut());
builder.body(response.bytes().await.unwrap()).unwrap()
}

#[tokio::main]
async fn main() {
let http_client = reqwest::Client::new();
let mut rng = ThreadRng::default();
let mut state = [0u8; 64];
rng.fill_bytes(&mut state);
let state_str = data_encoding::BASE64URL.encode(&state);

let mut verifier = [0u8; 32];
rng.fill_bytes(&mut verifier);
let verifier_str = data_encoding::BASE64URL_NOPAD.encode(&verifier);
let challenge_digest = ring::digest::digest(&ring::digest::SHA256, verifier_str.as_bytes());
let challenge = data_encoding::BASE64URL_NOPAD.encode(challenge_digest.as_ref());
let challenge_method = "S256".to_string();

let issuer_domain = std::env::var("ISSUER_DOMAIN").unwrap();
// Secret is optional in the PKCE flow
let client_secret = std::env::var("CLIENT_SECRET").ok();
let client_id = std::env::var("CLIENT_ID").unwrap();
let host = "127.0.0.1";
let port = 8000u16;
// It's very important that this exactly matches where it's provided in other places, protocol and trailing slash all
let redirect_uri = format!("http://{host}:{port}/");

// Fetch and instantiate a provider using a `well-known` uri from an issuer
let request = provider::well_known(&issuer_domain).unwrap();
let response = http_send(&http_client, request).await;
let provider = Provider::from_response(response).unwrap();
let auth_endpoint = provider.authorization_endpoint.to_string();
// 1. Authenticate through web browser
// user goes to embark auth url in browser
// auth service returns auth_code to listener at `redirect_uri`
// Add idp-specific extra query-parameters to the below `authorize_url`
let authorize_url = format!(
"{auth_endpoint}?\
code_challenge={challenge}&\
code_challenge_method=S256&\
response_type=code&\
client_id={client_id}&\
redirect_uri={redirect_uri}&\
state={state_str}&\
scope=openid+offline",
);
println!("Authorize at {authorize_url}");

let auth_code = listener(host, port).await;
println!("Listener closed down");
println!("Final code {}", auth_code);

// 3. User now has 2 minutes to swap the auth code for an Embark Access token.
// Make a `POST` request to the auth service /oauth2/token
let scheme = AuthenticationScheme::Pkce(PkceCredentials::new(
challenge.clone(),
challenge_method.clone(),
verifier_str.clone(),
client_secret.clone(),
));
let client_authentication = ClientAuthentication::new(client_id, scheme, None, None);
let exchange_request = provider
.exchange_token_request(&redirect_uri, &client_authentication, &auth_code)
.unwrap();

let response = http_send(&http_client, exchange_request).await;

// construct the response
let access_token = Token::from_response(response).unwrap();

// 4. Fetch the required JWKs
let request = provider.jwks_request().unwrap();
let response = http_send(&http_client, request).await;
let jwks = JWKS::from_response(response).unwrap();

let token_data = provider::verify_token::<Claims>(&access_token.access_token, &jwks.keys);
dbg!(&token_data);
dbg!(&access_token);
let refresh_token = access_token.refresh_token.unwrap();

// 5. Refresh token
let request = provider
.refresh_token_request(&client_authentication, &refresh_token)
.unwrap();
let response = http_send(&http_client, request).await;
let new_refresh_token = Token::from_response(response).unwrap();
dbg!(&new_refresh_token);
}
34 changes: 34 additions & 0 deletions src/deserialize_uri.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use http::Uri;
use serde::de::Error;
use serde::{de, Deserializer};
use std::fmt;
use std::fmt::Formatter;

struct UriVisitor;
impl<'de> de::Visitor<'de> for UriVisitor {
Expand All @@ -22,3 +24,35 @@ where
{
de.deserialize_str(UriVisitor)
}

struct OptUriVisitor;

impl<'de> de::Visitor<'de> for OptUriVisitor {
type Value = Option<Uri>;

fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
write!(formatter, "valid or missing uri")
}

#[inline]
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(Some(UriVisitor.visit_str(v)?))
}

fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: Error,
{
Ok(None)
}
}

pub fn deserialize_opt<'de, D>(de: D) -> Result<Option<Uri>, D::Error>
where
D: Deserializer<'de>,
{
de.deserialize_str(OptUriVisitor)
}
3 changes: 3 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub enum OidcValidationError {
#[error("Nonce doesn't match initially provided nonce")]
NonceMismatch,

#[error("Provider did not contain a userinfo endpoint")]
NoUserEndpoint,

#[error("Sub from user data doesn't match sub from token data")]
UserMismatch,

Expand Down
Loading

0 comments on commit 9ecded1

Please sign in to comment.