Skip to content

Commit

Permalink
Rewrite Google OpenIdProvider as trait implementation and add more ex…
Browse files Browse the repository at this point in the history
…tensive test coverage.
  • Loading branch information
sea-snake committed Jan 14, 2025
1 parent f1c077d commit e329742
Show file tree
Hide file tree
Showing 11 changed files with 335 additions and 196 deletions.
1 change: 0 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ ARG II_FETCH_ROOT_KEY=
ARG II_DUMMY_CAPTCHA=
ARG II_DUMMY_AUTH=
ARG II_DEV_CSP=
ARG II_OPENID_GOOGLE_CLIENT_ID=

RUN touch src/*/src/lib.rs
RUN npm ci
Expand Down
2 changes: 1 addition & 1 deletion dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "custom",
"candid": "src/internet_identity/internet_identity.did",
"wasm": "internet_identity.wasm.gz",
"build": "bash -c 'II_DEV_CSP=1 II_FETCH_ROOT_KEY=1 II_DUMMY_CAPTCHA=${II_DUMMY_CAPTCHA:-1} II_OPENID_GOOGLE_CLIENT_ID=\"45431994619-cbbfgtn7o0pp0dpfcg2l66bc4rcg7qbu.apps.googleusercontent.com\" scripts/build'",
"build": "bash -c 'II_DEV_CSP=1 II_FETCH_ROOT_KEY=1 II_DUMMY_CAPTCHA=${II_DUMMY_CAPTCHA:-1} scripts/build'",
"init_arg": "(opt record { captcha_config = opt record { max_unsolved_captchas= 50:nat64; captcha_trigger = variant {Static = variant {CaptchaDisabled}}}})",
"shrink": false
},
Expand Down
2 changes: 0 additions & 2 deletions scripts/build
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@ function build_canister() {
echo Running cargo build "${cargo_build_args[@]}"
echo RUSTFLAGS: "$RUSTFLAGS"

[ -n "${II_OPENID_GOOGLE_CLIENT_ID:-}" ] && export II_OPENID_GOOGLE_CLIENT_ID

RUSTFLAGS="$RUSTFLAGS" cargo build "${cargo_build_args[@]}"

if [ "$ONLY_DEPS" != "1" ]
Expand Down
17 changes: 0 additions & 17 deletions src/internet_identity/build.rs

This file was deleted.

2 changes: 0 additions & 2 deletions src/internet_identity/src/constants.rs

This file was deleted.

11 changes: 8 additions & 3 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ mod archive;
mod assets;
mod authz_utils;

mod constants;
/// Type conversions between internal and external types.
mod conversions;
mod delegation;
Expand Down Expand Up @@ -349,6 +348,7 @@ fn config() -> InternetIdentityInit {
register_rate_limit: Some(persistent_state.registration_rate_limit.clone()),
captcha_config: Some(persistent_state.captcha_config.clone()),
related_origins: persistent_state.related_origins.clone(),
openid_google_client_id: persistent_state.openid_google_client_id.clone(),
})
}

Expand Down Expand Up @@ -388,15 +388,20 @@ fn post_upgrade(maybe_arg: Option<InternetIdentityInit>) {
}

fn initialize(maybe_arg: Option<InternetIdentityInit>) {
let state_related_origins = state::persistent_state(|storage| storage.related_origins.clone());
let state_related_origins = persistent_state(|storage| storage.related_origins.clone());
let related_origins = maybe_arg
.clone()
.map(|arg| arg.related_origins)
.unwrap_or(state_related_origins);
let state_openid_google_client_id = persistent_state(|storage| storage.openid_google_client_id.clone());
let openid_google_client_id = maybe_arg
.clone()
.map(|arg| arg.openid_google_client_id)
.unwrap_or(state_openid_google_client_id);
init_assets(related_origins);
apply_install_arg(maybe_arg);
update_root_hash();
openid::setup_timers();
openid_google_client_id.map(openid::setup_google);
}

fn apply_install_arg(maybe_arg: Option<InternetIdentityInit>) {
Expand Down
106 changes: 95 additions & 11 deletions src/internet_identity/src/openid.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use candid::{Deserialize, Principal};
use identity_jose::jws::Decoder;
use internet_identity_interface::internet_identity::types::{MetadataEntryV2, Timestamp};
use std::cell::RefCell;
use std::collections::HashMap;

mod google;
Expand All @@ -15,29 +16,112 @@ pub struct OpenIdCredential {
pub metadata: HashMap<String, MetadataEntryV2>,
}

trait OpenIdProvider {
fn issuer(&self) -> &'static str;

fn verify(&self, jwt: &str, salt: &[u8; 32]) -> Result<OpenIdCredential, String>;
}

#[derive(Deserialize)]
struct PartialClaims {
iss: String,
}

pub fn setup_timers() {
google::setup_timers();
thread_local! {
static OPEN_ID_PROVIDERS: RefCell<Vec<Box<dyn OpenIdProvider >>> = RefCell::new(vec![]);
}

pub fn setup_google(client_id: String) {
OPEN_ID_PROVIDERS.with(|providers| {
providers
.borrow_mut()
.push(Box::new(google::Provider::create(client_id)));
});
}

#[allow(unused)]
pub fn verify(
jwt: &str,
session_principal: &Principal,
session_salt: &[u8; 32],
timestamp: Timestamp,
) -> Result<OpenIdCredential, String> {
pub fn verify(jwt: &str, salt: &[u8; 32]) -> Result<OpenIdCredential, String> {
let validation_item = Decoder::new()
.decode_compact_serialization(jwt.as_bytes(), None)
.map_err(|_| "Failed to decode JWT")?;
let claims: PartialClaims =
serde_json::from_slice(validation_item.claims()).map_err(|_| "Unable to decode claims")?;
match claims.iss.as_str() {
google::ISSUER => google::verify(jwt, session_principal, session_salt, timestamp),
_ => Err(format!("Unsupported issuer: {}", claims.iss)),

OPEN_ID_PROVIDERS.with(|providers| {
match providers
.borrow()
.iter()
.find(|provider| provider.issuer() == claims.iss)
{
Some(provider) => provider.verify(jwt, salt),
None => Err(format!("Unsupported issuer: {}", claims.iss)),
}
})
}

#[cfg(test)]
struct ExampleProvider;
#[cfg(test)]
impl OpenIdProvider for ExampleProvider {
fn issuer(&self) -> &'static str {
"https://example.com"
}

fn verify(&self, _: &str, _: &[u8; 32]) -> Result<OpenIdCredential, String> {
Ok(self.credential())
}
}
#[cfg(test)]
impl ExampleProvider {
fn credential(&self) -> OpenIdCredential {
OpenIdCredential {
iss: self.issuer().into(),
sub: "example-sub".into(),
aud: "example-aud".into(),
principal: Principal::anonymous(),
last_usage_timestamp: 0,
metadata: HashMap::new(),
}
}
}

#[test]
fn should_return_credential() {
let provider = ExampleProvider {};
let credential = provider.credential();
OPEN_ID_PROVIDERS.replace(vec![Box::new(provider)]);
let jwt = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIn0.SBeD7pV65F98wStsBuC_VRn-yjLoyf6iojJl9Y__wN0";

assert_eq!(verify(jwt, &[0u8; 32]), Ok(credential));
}

#[test]
fn should_return_error_unsupported_issuer() {
OPEN_ID_PROVIDERS.replace(vec![]);
let jwt = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIn0.SBeD7pV65F98wStsBuC_VRn-yjLoyf6iojJl9Y__wN0";

assert_eq!(
verify(jwt, &[0u8; 32]),
Err("Unsupported issuer: https://example.com".into())
);
}

#[test]
fn should_return_error_when_encoding_invalid() {
let invalid_jwt = "invalid-jwt";

assert_eq!(
verify(invalid_jwt, &[0u8; 32]),
Err("Failed to decode JWT".to_string())
);
}

#[test]
fn should_return_error_when_claims_invalid() {
let jwt_without_issuer = "eyJhbGciOiJIUzI1NiJ9.e30.ZRrHA1JJJW8opsbCGfG_HACGpVUMN_a9IV7pAx_Zmeo";

assert_eq!(
verify(jwt_without_issuer, &[0u8; 32]),
Err("Unable to decode claims".to_string())
);
}
Loading

0 comments on commit e329742

Please sign in to comment.