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

Implement Google JWT verification and OpenID Google client id init parameter. #2780

Merged
merged 22 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7ab4c76
Implement Google JWT verification and build time environment variables.
sea-snake Jan 13, 2025
9bb803d
🤖 cargo-fmt auto-update
github-actions[bot] Jan 13, 2025
8764a0f
Make env variable optional
sea-snake Jan 13, 2025
90687f3
Merge remote-tracking branch 'origin/sea-snake/verify-google-jwt' int…
sea-snake Jan 13, 2025
9b22b5c
Make env variable optional
sea-snake Jan 13, 2025
d911471
Make env variable optional
sea-snake Jan 13, 2025
1ed3883
Fix clippy complaints.
sea-snake Jan 13, 2025
f1c077d
🤖 cargo-fmt auto-update
github-actions[bot] Jan 13, 2025
e329742
Rewrite Google OpenIdProvider as trait implementation and add more ex…
sea-snake Jan 14, 2025
d363e8c
🤖 cargo-fmt auto-update
github-actions[bot] Jan 14, 2025
9de1550
Rewrite Google OpenIdProvider as trait implementation and add more ex…
sea-snake Jan 14, 2025
0271314
Merge remote-tracking branch 'origin/sea-snake/verify-google-jwt' int…
sea-snake Jan 14, 2025
8313d15
Fix integration tests
sea-snake Jan 14, 2025
e52ce03
Simplify initialize
sea-snake Jan 14, 2025
94526a3
Add OpenID to config integration tests
sea-snake Jan 15, 2025
288286f
🤖 cargo-fmt auto-update
github-actions[bot] Jan 15, 2025
75f87ad
🤖 npm run generate auto-update
github-actions[bot] Jan 15, 2025
8b29f4f
Fix clippy
sea-snake Jan 15, 2025
299205a
Merge remote-tracking branch 'origin/sea-snake/verify-google-jwt' int…
sea-snake Jan 15, 2025
ad5ab92
🤖 cargo-fmt auto-update
github-actions[bot] Jan 15, 2025
b4140a6
Merge branch 'main' into sea-snake/verify-google-jwt
sea-snake Jan 15, 2025
e6d6de1
Fix integration test
sea-snake Jan 15, 2025
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
80 changes: 80 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ serde = "1"
serde_bytes = "0.11"
serde_cbor = "0.11"
sha2 = "0.10"
rsa = "0.9.7"
4 changes: 2 additions & 2 deletions dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"wasm": "internet_identity.wasm.gz",
"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
"shrink": false
},
"test_app": {
"type": "custom",
Expand All @@ -20,7 +20,7 @@
"wasm": "demos/vc_issuer/vc_demo_issuer.wasm.gz",
"build": "demos/vc_issuer/build.sh",
"post_install": "bash -c 'demos/vc_issuer/provision'",
"dependencies": [ "internet_identity" ]
"dependencies": ["internet_identity"]
}
},
"defaults": {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"private": true,
"license": "SEE LICENSE IN LICENSE.md",
"scripts": {
"dev": "II_FETCH_ROOT_KEY=1 II_DUMMY_CAPTCHA=1 II_OPENID_GOOGLE_CLIENT_ID=45431994619-cbbfgtn7o0pp0dpfcg2l66bc4rcg7qbu.apps.googleusercontent.com vite",
"host": "II_FETCH_ROOT_KEY=1 II_DUMMY_CAPTCHA=1 II_OPENID_GOOGLE_CLIENT_ID=45431994619-cbbfgtn7o0pp0dpfcg2l66bc4rcg7qbu.apps.googleusercontent.com vite --host",
"dev": "II_FETCH_ROOT_KEY=1 II_DUMMY_CAPTCHA=1 II_OPENID_GOOGLE_CLIENT_ID=\"45431994619-cbbfgtn7o0pp0dpfcg2l66bc4rcg7qbu.apps.googleusercontent.com\" vite",
"host": "II_FETCH_ROOT_KEY=1 II_DUMMY_CAPTCHA=1 II_OPENID_GOOGLE_CLIENT_ID=\"45431994619-cbbfgtn7o0pp0dpfcg2l66bc4rcg7qbu.apps.googleusercontent.com\" vite --host",
"showcase": "astro dev --root ./src/showcase",
"build": "tsc --noEmit && vite build",
"check": "tsc --project ./tsconfig.all.json --noEmit",
Expand Down
3 changes: 2 additions & 1 deletion src/internet_identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ serde.workspace = true
serde_bytes.workspace = true
serde_cbor.workspace = true
serde_json = { version = "1.0", default-features = false, features = ["std"] }
sha2.workspace = true
sha2 = { workspace = true, features = ["oid"]}
base64.workspace = true
rsa.workspace = true

# Captcha deps
lodepng = "*"
Expand Down
18 changes: 12 additions & 6 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,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 @@ -387,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 related_origins = maybe_arg
.clone()
.map(|arg| arg.related_origins)
.unwrap_or(state_related_origins);
let related_origins = maybe_arg.clone().map_or_else(
|| persistent_state(|storage| storage.related_origins.clone()),
|arg| arg.related_origins,
);
let openid_google_client_id = maybe_arg.clone().map_or_else(
|| persistent_state(|storage| storage.openid_google_client_id.clone()),
|arg| arg.openid_google_client_id,
);
init_assets(related_origins);
apply_install_arg(maybe_arg);
update_root_hash();
openid::setup_timers();
if let Some(client_id) = openid_google_client_id {
openid::setup_google(client_id);
}
}

fn apply_install_arg(maybe_arg: Option<InternetIdentityInit>) {
Expand Down
126 changes: 124 additions & 2 deletions src/internet_identity/src/openid.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,127 @@
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;

pub fn setup_timers() {
google::setup_timers();
#[derive(Debug, PartialEq)]
pub struct OpenIdCredential {
pub iss: String,
pub sub: String,
pub aud: String,
pub principal: Principal,
pub last_usage_timestamp: Timestamp,
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,
}

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()
sea-snake marked this conversation as resolved.
Show resolved Hide resolved
.push(Box::new(google::Provider::create(client_id)));
});
}

#[allow(unused)]
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")?;

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)]
sea-snake marked this conversation as resolved.
Show resolved Hide resolved
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
Loading