Skip to content
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

Set audience validation default to off #25

Merged
merged 8 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@ 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
## [0.7.0] - Unreleased
### 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
Loading