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

Add and remove OpenID credentials from anchor #2810

Merged
merged 69 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
464c0d2
Add OpenID credential storage in stable structures and anchor managem…
sea-snake Jan 22, 2025
7db91b5
- OpenID credentials in stable structures.
sea-snake Jan 22, 2025
7e749e7
🤖 cargo-fmt auto-update
github-actions[bot] Jan 22, 2025
1a92adb
🤖 npm run generate auto-update
github-actions[bot] Jan 22, 2025
cca4497
Fix error in archive operation compat
sea-snake Jan 22, 2025
eaf5646
Fix error in anchor tests
sea-snake Jan 22, 2025
db90889
Add OpenID credentials to did.
sea-snake Jan 22, 2025
b3534cb
🤖 npm run generate auto-update
github-actions[bot] Jan 22, 2025
98e3455
Add OpenID credentials operations to archive did.
sea-snake Jan 22, 2025
279a73a
Merge remote-tracking branch 'origin/sea-snake/openid-google-add-remo…
sea-snake Jan 22, 2025
28e4640
Make OpenID credential in StableAnchor not optional.
sea-snake Jan 22, 2025
04117db
Make OpenID credentials in IdentityAnchorInfo optional so it's backwa…
sea-snake Jan 22, 2025
072252c
🤖 cargo-fmt auto-update
github-actions[bot] Jan 22, 2025
80d94ac
Make OpenID credentials in IdentityAnchorInfo optional so it's backwa…
sea-snake Jan 22, 2025
3042122
🤖 npm run generate auto-update
github-actions[bot] Jan 22, 2025
a555694
Fix principal from seed in tests
sea-snake Jan 22, 2025
bed886e
🤖 cargo-fmt auto-update
github-actions[bot] Jan 22, 2025
114d254
Changes based on feedback and added tests for new `Anchor` methods.
sea-snake Jan 23, 2025
2a6827b
🤖 cargo-fmt auto-update
github-actions[bot] Jan 23, 2025
4639e4e
Fix serde name for archive operation.
sea-snake Jan 23, 2025
5a7e8ef
Merge remote-tracking branch 'origin/sea-snake/openid-google-add-remo…
sea-snake Jan 23, 2025
aa1f722
Wrap tuple with struct since stable-structures doesn't directly suppo…
sea-snake Jan 23, 2025
0049fbc
🤖 cargo-fmt auto-update
github-actions[bot] Jan 23, 2025
5c47f82
Update based on feedback.
sea-snake Jan 23, 2025
3c78479
Update based on feedback.
sea-snake Jan 23, 2025
810af38
🤖 cargo-fmt auto-update
github-actions[bot] Jan 23, 2025
7c82534
Update based on feedback.
sea-snake Jan 23, 2025
e27c0ab
Merge remote-tracking branch 'origin/sea-snake/openid-google-add-remo…
sea-snake Jan 23, 2025
91a5432
🤖 cargo-fmt auto-update
github-actions[bot] Jan 23, 2025
73a061c
Update based on feedback.
sea-snake Jan 23, 2025
6280d3b
Merge remote-tracking branch 'origin/sea-snake/openid-google-add-remo…
sea-snake Jan 23, 2025
bb5b426
Update based on feedback.
sea-snake Jan 23, 2025
5e458d3
Update based on feedback.
sea-snake Jan 23, 2025
94026e7
Update based on feedback.
sea-snake Jan 23, 2025
41be074
Make lookup map many-to-many
sea-snake Jan 27, 2025
5157a1c
Make lookup map many-to-many
sea-snake Jan 27, 2025
34a8b8f
🤖 cargo-fmt auto-update
github-actions[bot] Jan 27, 2025
2234e72
🤖 npm run generate auto-update
github-actions[bot] Jan 27, 2025
9f061b7
Make lookup map many-to-many
sea-snake Jan 27, 2025
cfe4ffd
Merge remote-tracking branch 'origin/sea-snake/openid-google-add-remo…
sea-snake Jan 27, 2025
efac984
Implement canister methods and link to frontend to add and remove Ope…
sea-snake Jan 23, 2025
81874c3
Implement canister methods and link to frontend to add and remove Ope…
sea-snake Jan 23, 2025
4799a0e
WIP
sea-snake Jan 24, 2025
e1eb9af
WIP
sea-snake Jan 24, 2025
4257fad
Undo changes
sea-snake Jan 27, 2025
51c573b
Undo changes
sea-snake Jan 27, 2025
985939d
Add check to make sure OpenID credential is only added to a single an…
sea-snake Jan 27, 2025
0a74291
WIP
sea-snake Jan 24, 2025
18bb775
WIP
sea-snake Jan 24, 2025
0631fe2
Undo changes
sea-snake Jan 27, 2025
7f90e07
Undo changes
sea-snake Jan 27, 2025
770168a
🤖 cargo-fmt auto-update
github-actions[bot] Jan 27, 2025
f045a05
Fix import
sea-snake Jan 27, 2025
bfa4f0c
Merge remote-tracking branch 'origin/sea-snake/openid-google-add-remo…
sea-snake Jan 27, 2025
f7614a0
Add InternalCanisterError to error enums similar to v2 api.
sea-snake Jan 27, 2025
e6aec3e
Fix did file.
sea-snake Jan 27, 2025
8bfe986
🤖 npm run generate auto-update
github-actions[bot] Jan 27, 2025
37340fb
Remove frontend env variable and use backend config instead.
sea-snake Jan 28, 2025
08e0bb5
Merge remote-tracking branch 'origin/sea-snake/openid-google-add-remo…
sea-snake Jan 28, 2025
6c27565
Merge branch 'main' into sea-snake/openid-google-add-remove-from-anchor
sea-snake Jan 28, 2025
2268e5d
Remove `#[allow(unused)]` from methods that are now used.
sea-snake Jan 28, 2025
223447f
Update translation labels based on feedback.
sea-snake Jan 28, 2025
abd4a04
Add TODO comment.
sea-snake Jan 28, 2025
0a1a7d7
Add `should_register_openid_credential_only_for_a_single_anchor` test.
sea-snake Jan 28, 2025
69a7ac7
🤖 cargo-fmt auto-update
github-actions[bot] Jan 28, 2025
1b47942
Add `should_register_openid_credential_only_for_a_single_anchor` test.
sea-snake Jan 28, 2025
eb20b55
Merge remote-tracking branch 'origin/sea-snake/openid-google-add-remo…
sea-snake Jan 28, 2025
fcbef18
Move imports into test itself.
sea-snake Jan 28, 2025
0da1a61
Fix test
sea-snake Jan 28, 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
6 changes: 5 additions & 1 deletion src/canister_tests/src/api/archive.rs
sea-snake marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ pub mod compat {
new_device,
},
Operation::RemoveDevice { device } => CompatOperation::RemoveDevice { device },
Operation::IdentityMetadataReplace { .. } => panic!("not available in compat type"),
Operation::IdentityMetadataReplace { .. }
| Operation::AddOpenIdCredential { .. }
| Operation::RemoveOpenIdCredential { .. } => {
panic!("not available in compat type")
}
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/frontend/generated/internet_identity_idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,21 @@ export const idlFactory = ({ IDL }) => {
'purpose' : Purpose,
'credential_id' : IDL.Opt(CredentialId),
});
const OpenIdCredential = IDL.Record({
'aud' : IDL.Text,
'iss' : IDL.Text,
'sub' : IDL.Text,
'delegation_principal' : IDL.Principal,
'metadata' : MetadataMapV2,
'last_usage_timestamp' : Timestamp,
});
const DeviceRegistrationInfo = IDL.Record({
'tentative_device' : IDL.Opt(DeviceData),
'expiration' : Timestamp,
});
const IdentityAnchorInfo = IDL.Record({
'devices' : IDL.Vec(DeviceWithUsage),
'openid_credentials' : IDL.Vec(OpenIdCredential),
'device_registration' : IDL.Opt(DeviceRegistrationInfo),
});
const FrontendHostname = IDL.Text;
Expand Down
9 changes: 9 additions & 0 deletions src/frontend/generated/internet_identity_types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export type IdRegStartError = { 'InvalidCaller' : null } |
{ 'RateLimitExceeded' : null };
export interface IdentityAnchorInfo {
'devices' : Array<DeviceWithUsage>,
'openid_credentials' : Array<OpenIdCredential>,
'device_registration' : [] | [DeviceRegistrationInfo],
}
export interface IdentityAuthnInfo {
Expand Down Expand Up @@ -238,6 +239,14 @@ export type MetadataMapV2 = Array<
]
>;
export interface OpenIdConfig { 'client_id' : string }
export interface OpenIdCredential {
'aud' : string,
'iss' : string,
'sub' : string,
'delegation_principal' : Principal,
'metadata' : MetadataMapV2,
'last_usage_timestamp' : Timestamp,
}
export type PrepareIdAliasError = { 'InternalCanisterError' : string } |
{ 'Unauthorized' : Principal };
export interface PrepareIdAliasRequest {
Expand Down
11 changes: 11 additions & 0 deletions src/internet_identity/internet_identity.did
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,8 @@ type IdentityAnchorInfo = record {
devices : vec DeviceWithUsage;
// Device registration status used when adding devices, see DeviceRegistrationInfo
device_registration: opt DeviceRegistrationInfo;
// OpenID accounts linked to this anchor
openid_credentials: vec OpenIdCredential;
};

type AnchorCredentials = record {
Expand Down Expand Up @@ -327,6 +329,15 @@ type OpenIdConfig = record {
client_id: text;
};

type OpenIdCredential = record {
iss: text;
sub: text;
aud: text;
delegation_principal: principal;
last_usage_timestamp: Timestamp;
metadata: MetadataMapV2;
};

// API V2 specific types
// WARNING: These type are experimental and may change in the future.

Expand Down
59 changes: 52 additions & 7 deletions src/internet_identity/src/anchor_management.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
use crate::archive::{archive_operation, device_diff};
use crate::openid::OpenIdCredential;
use crate::state::RegistrationState::DeviceTentativelyAdded;
use crate::state::TentativeDeviceRegistration;
use crate::storage::anchor::{Anchor, AnchorError, Device};
use crate::{state, stats::activity_stats};
use ic_cdk::api::time;
use ic_cdk::{caller, trap};
use internet_identity_interface::archive::types::{DeviceDataWithoutAlias, Operation};
use internet_identity_interface::internet_identity::types::*;
use internet_identity_interface::internet_identity::types::openid::OpenIdCredentialData;
use internet_identity_interface::internet_identity::types::{
AnchorNumber, DeviceData, DeviceKey, DeviceRegistrationInfo, DeviceWithUsage,
IdentityAnchorInfo, MetadataEntry,
};
use std::collections::HashMap;

pub mod registration;
pub mod tentative_device_registration;

pub fn get_anchor_info(anchor_number: AnchorNumber) -> IdentityAnchorInfo {
let devices = state::anchor(anchor_number)
.into_devices()
let anchor = state::anchor(anchor_number);
let devices = anchor
.devices()
.clone()
.into_iter()
.map(DeviceWithUsage::from)
.collect();
let openid_credentials = anchor
.openid_credentials()
.clone()
.into_iter()
.map(OpenIdCredentialData::from)
.collect();
let now = time();

state::tentative_device_registrations(|tentative_device_registrations| {
Expand All @@ -34,6 +47,7 @@ pub fn get_anchor_info(anchor_number: AnchorNumber) -> IdentityAnchorInfo {
expiration: *expiration,
tentative_device: Some(tentative_device.clone()),
}),
openid_credentials,
},
Some(TentativeDeviceRegistration { expiration, .. }) if *expiration > now => {
IdentityAnchorInfo {
Expand All @@ -42,11 +56,13 @@ pub fn get_anchor_info(anchor_number: AnchorNumber) -> IdentityAnchorInfo {
expiration: *expiration,
tentative_device: None,
}),
openid_credentials,
}
}
None | Some(_) => IdentityAnchorInfo {
devices,
device_registration: None,
openid_credentials,
},
}
})
Expand Down Expand Up @@ -81,7 +97,7 @@ pub fn post_operation_bookkeeping(anchor_number: AnchorNumber, operation: Operat

/// Adds a device to the given anchor and returns the operation to be archived.
/// Panics if this operation violates anchor constraints (see [Anchor]).
pub fn add(anchor: &mut Anchor, device_data: DeviceData) -> Operation {
pub fn add_device(anchor: &mut Anchor, device_data: DeviceData) -> Operation {
let new_device = Device::from(device_data);
anchor
.add_device(new_device.clone())
Expand All @@ -96,7 +112,11 @@ pub fn add(anchor: &mut Anchor, device_data: DeviceData) -> Operation {
/// Panics if
/// * the device to be updated does not exist
/// * the operation violates anchor constraints (see [Anchor])
pub fn update(anchor: &mut Anchor, device_key: DeviceKey, device_data: DeviceData) -> Operation {
pub fn update_device(
anchor: &mut Anchor,
device_key: DeviceKey,
device_data: DeviceData,
) -> Operation {
let Some(existing_device) = anchor.device(&device_key) else {
trap("Could not find device to update, check device key")
};
Expand All @@ -119,7 +139,7 @@ pub fn update(anchor: &mut Anchor, device_key: DeviceKey, device_data: DeviceDat
/// Panics if
/// * the device to be replaced does not exist
/// * the operation violates anchor constraints (see [Anchor])
pub fn replace(
pub fn replace_device(
anchor_number: AnchorNumber,
anchor: &mut Anchor,
old_device: DeviceKey,
Expand All @@ -142,7 +162,7 @@ pub fn replace(

/// Removes a device of the given anchor and returns the operation to be archived.
/// Panics if the device to be removed does not exist
pub fn remove(
pub fn remove_device(
anchor_number: AnchorNumber,
anchor: &mut Anchor,
device_key: DeviceKey,
Expand All @@ -165,3 +185,28 @@ pub fn identity_metadata_replace(
anchor.replace_identity_metadata(metadata)?;
Ok(Operation::IdentityMetadataReplace { metadata_keys })
}

/// Adds an `OpenIdCredential` to the given anchor and returns the operation to be archived.
/// Returns an error if the `OpenIdCredential` already exists.
#[allow(unused)]
sea-snake marked this conversation as resolved.
Show resolved Hide resolved
pub fn add_openid_credential(
anchor: &mut Anchor,
openid_credential: OpenIdCredential,
) -> Result<Operation, AnchorError> {
anchor.add_openid_credential(openid_credential.clone())?;
Ok(Operation::AddOpenIdCredential {
iss: openid_credential.iss,
})
}

/// Removes an `OpenIdCredential` of the given anchor and returns the operation to be archived.
/// Return an error if the `OpenIdCredential` to be removed does not exist.
#[allow(unused)]
sea-snake marked this conversation as resolved.
Show resolved Hide resolved
pub fn remove_openid_credential(
anchor: &mut Anchor,
iss: &str,
sub: &str,
) -> Result<Operation, AnchorError> {
anchor.remove_openid_credential(iss, sub)?;
Ok(Operation::RemoveOpenIdCredential { iss: iss.into() })
sea-snake marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::anchor_management::add;
use crate::anchor_management::add_device;
use crate::authz_utils::IdentityUpdateError;
use crate::state::RegistrationState::{DeviceRegistrationModeActive, DeviceTentativelyAdded};
use crate::state::TentativeDeviceRegistration;
Expand Down Expand Up @@ -129,7 +129,7 @@ pub fn verify_tentative_device(
) -> Result<((), Operation), VerifyTentativeDeviceError> {
match get_verified_device(anchor_number, user_verification_code) {
Ok(device) => {
let operation = add(anchor, device);
let operation = add_device(anchor, device);
Ok(((), operation))
}
Err(err) => Err(err),
Expand Down
8 changes: 4 additions & 4 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ fn register(
#[update]
fn add(anchor_number: AnchorNumber, device_data: DeviceData) {
anchor_operation_with_authz_check(anchor_number, |anchor| {
Ok::<_, String>(((), anchor_management::add(anchor, device_data)))
Ok::<_, String>(((), anchor_management::add_device(anchor, device_data)))
})
.unwrap_or_else(|err| trap(err.as_str()))
}
Expand All @@ -163,7 +163,7 @@ fn update(anchor_number: AnchorNumber, device_key: DeviceKey, device_data: Devic
anchor_operation_with_authz_check(anchor_number, |anchor| {
Ok::<_, String>((
(),
anchor_management::update(anchor, device_key, device_data),
anchor_management::update_device(anchor, device_key, device_data),
))
})
.unwrap_or_else(|err| trap(err.as_str()))
Expand All @@ -174,7 +174,7 @@ fn replace(anchor_number: AnchorNumber, device_key: DeviceKey, device_data: Devi
anchor_operation_with_authz_check(anchor_number, |anchor| {
Ok::<_, String>((
(),
anchor_management::replace(anchor_number, anchor, device_key, device_data),
anchor_management::replace_device(anchor_number, anchor, device_key, device_data),
))
})
.unwrap_or_else(|err| trap(err.as_str()))
Expand All @@ -185,7 +185,7 @@ fn remove(anchor_number: AnchorNumber, device_key: DeviceKey) {
anchor_operation_with_authz_check(anchor_number, |anchor| {
Ok::<_, String>((
(),
anchor_management::remove(anchor_number, anchor, device_key),
anchor_management::remove_device(anchor_number, anchor, device_key),
))
})
.unwrap_or_else(|err| trap(err.as_str()))
Expand Down
60 changes: 48 additions & 12 deletions src/internet_identity/src/openid.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
use candid::{Deserialize, Principal};
use crate::delegation::der_encode_canister_sig_key;
use crate::state;
use candid::{CandidType, Deserialize, Principal};
use ic_certification::Hash;
use identity_jose::jws::Decoder;
use internet_identity_interface::internet_identity::types::{
MetadataEntryV2, OpenIdConfig, Timestamp,
};
use sha2::{Digest, Sha256};
use std::cell::RefCell;
use std::collections::HashMap;

mod google;

#[derive(Debug, PartialEq)]
pub type Iss = String;
pub type Sub = String;
pub type Aud = String;

sea-snake marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Debug, PartialEq, Eq, CandidType, Deserialize, Clone)]
pub struct OpenIdCredential {
pub iss: String,
pub sub: String,
pub aud: String,
pub principal: Principal,
pub iss: Iss,
pub sub: Sub,
pub aud: Aud,
pub delegation_principal: Principal,
pub last_usage_timestamp: Timestamp,
sea-snake marked this conversation as resolved.
Show resolved Hide resolved
pub metadata: HashMap<String, MetadataEntryV2>,
}
Expand All @@ -30,11 +38,11 @@ struct PartialClaims {
}

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

pub fn setup_google(config: OpenIdConfig) {
OPEN_ID_PROVIDERS
PROVIDERS
.with_borrow_mut(|providers| providers.push(Box::new(google::Provider::create(config))));
}

Expand All @@ -46,7 +54,7 @@ pub fn verify(jwt: &str, salt: &[u8; 32]) -> Result<OpenIdCredential, String> {
let claims: PartialClaims =
serde_json::from_slice(validation_item.claims()).map_err(|_| "Unable to decode claims")?;

OPEN_ID_PROVIDERS.with_borrow(|providers| {
PROVIDERS.with_borrow(|providers| {
match providers
.iter()
.find(|provider| provider.issuer() == claims.iss)
Expand All @@ -57,6 +65,34 @@ pub fn verify(jwt: &str, salt: &[u8; 32]) -> Result<OpenIdCredential, String> {
})
}

#[allow(clippy::cast_possible_truncation)]
fn calculate_seed(client_id: &str, iss: &Iss, sub: &Sub) -> Hash {
let salt = state::salt();
sea-snake marked this conversation as resolved.
Show resolved Hide resolved

let mut blob: Vec<u8> = vec![];
blob.push(32);
blob.extend_from_slice(&salt);

blob.push(client_id.bytes().len() as u8);
blob.extend(client_id.bytes());

blob.push(iss.bytes().len() as u8);
blob.extend(iss.bytes());

blob.push(sub.bytes().len() as u8);
blob.extend(sub.bytes());

let mut hasher = Sha256::new();
hasher.update(blob);
hasher.finalize().into()
}

fn get_delegation_principal(client_id: &str, iss: &Iss, sub: &Sub) -> Principal {
let seed = calculate_seed(client_id, iss, sub);
let public_key = der_encode_canister_sig_key(seed.to_vec());
Principal::self_authenticating(public_key)
}

#[cfg(test)]
struct ExampleProvider;

Expand All @@ -78,7 +114,7 @@ impl ExampleProvider {
iss: self.issuer().into(),
sub: "example-sub".into(),
aud: "example-aud".into(),
principal: Principal::anonymous(),
delegation_principal: Principal::anonymous(),
last_usage_timestamp: 0,
metadata: HashMap::new(),
}
Expand All @@ -89,15 +125,15 @@ impl ExampleProvider {
fn should_return_credential() {
let provider = ExampleProvider {};
let credential = provider.credential();
OPEN_ID_PROVIDERS.replace(vec![Box::new(provider)]);
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![]);
PROVIDERS.replace(vec![]);
let jwt = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIn0.SBeD7pV65F98wStsBuC_VRn-yjLoyf6iojJl9Y__wN0";

assert_eq!(
Expand Down
Loading
Loading