Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions Cargo.lock

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

14 changes: 14 additions & 0 deletions src/canister_tests/src/api/internet_identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,20 @@ pub fn config(
call_candid(env, canister_id, RawEffectivePrincipal::None, "config", ()).map(|(x,)| x)
}

pub fn discovered_oidc_configs(
env: &PocketIc,
canister_id: CanisterId,
) -> Result<Vec<types::OidcConfig>, RejectResponse> {
call_candid(
env,
canister_id,
RawEffectivePrincipal::None,
"discovered_oidc_configs",
(),
)
.map(|(x,)| x)
}

pub fn openid_prepare_delegation(
env: &PocketIc,
canister_id: CanisterId,
Expand Down
1 change: 1 addition & 0 deletions src/internet_identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ captcha = { git = "https://github.com/dfinity/captcha", rev = "9c0d2dd9bf519e255

# OpenID deps
identity_jose = { git = "https://github.com/dfinity/identity.rs.git", rev = "aa510ef7f441848d6c78058fe51ad4ad1d9bd5d8", default-features = false }
url = { version = "2.5", default-features = false, features = ["std"] }

# All IC deps
candid.workspace = true
Expand Down
26 changes: 26 additions & 0 deletions src/internet_identity/internet_identity.did
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@ type InternetIdentityInit = record {
new_flow_origins : opt vec text;
// Configurations for OpenID clients
openid_configs : opt vec OpenIdConfig;
// Simplified OIDC configs using discovery. Mutually exclusive with openid_configs.
oidc_configs : opt vec DiscoverableOidcConfig;
Comment thread
sea-snake marked this conversation as resolved.
// Configuration for Web Analytics
analytics_config : opt opt AnalyticsConfig;
// Configuration to show dapps explorer or not
Expand Down Expand Up @@ -383,6 +385,26 @@ type OpenIdConfig = record {
email_verification : opt OpenIdEmailVerification;
};

// Simplified OIDC provider config that relies on discovery.
// client_id must be set. discovery_url is the standard OIDC .well-known endpoint.
type DiscoverableOidcConfig = record {
name : text;
logo : text;
discovery_url : text;
client_id : opt text;
email_verification : opt OpenIdEmailVerification;
};

// Resolved OIDC config with discovered fields.
type OidcConfig = record {
name : text;
logo : text;
discovery_url : text;
client_id : opt text;
issuer : opt text;
email_verification : opt OpenIdEmailVerification;
};

type OpenIdCredentialKey = record { Iss; Sub };

type AnalyticsConfig = variant {
Expand Down Expand Up @@ -1175,6 +1197,10 @@ service : (opt InternetIdentityInit) -> {
// =====================
http_request : (request : HttpRequest) -> (HttpResponse) query;

// OIDC Discovery
// ===============
discovered_oidc_configs : () -> (vec OidcConfig) query;

// Internal Methods
// ================
init_salt : () -> ();
Expand Down
34 changes: 30 additions & 4 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,11 @@ fn stats() -> InternetIdentityStats {
})
}

#[query]
fn discovered_oidc_configs() -> Vec<OidcConfig> {
openid::get_discovered_oidc_configs()
}

#[query]
fn config() -> InternetIdentityInit {
let archive_config = match state::archive_state() {
Expand All @@ -568,6 +573,7 @@ fn config() -> InternetIdentityInit {
related_origins: persistent_state.related_origins.clone(),
new_flow_origins: persistent_state.new_flow_origins.clone(),
openid_configs: persistent_state.openid_configs.clone(),
oidc_configs: persistent_state.oidc_configs.clone(),
Comment thread
sea-snake marked this conversation as resolved.
analytics_config: Some(persistent_state.analytics_config.clone()),
enable_dapps_explorer: persistent_state.enable_dapps_explorer,
is_production: persistent_state.is_production,
Expand Down Expand Up @@ -625,6 +631,9 @@ fn initialize(maybe_arg: Option<InternetIdentityInit>) {
if let Some(openid_configs) = config.openid_configs {
openid::setup(openid_configs);
}
if let Some(oidc_configs) = config.oidc_configs {
openid::setup_oidc(oidc_configs);
}
}

fn apply_install_arg(maybe_arg: Option<InternetIdentityInit>) {
Expand Down Expand Up @@ -657,14 +666,31 @@ fn apply_install_arg(maybe_arg: Option<InternetIdentityInit>) {
persistent_state.related_origins = Some(related_origins);
})
}
if let Some(new_flow_origins) = arg.new_flow_origins {
// oidc_configs, openid_configs, and new_flow_origins are mutually exclusive.
Comment thread
sea-snake marked this conversation as resolved.
Outdated
// When openid_configs is provided alongside oidc_configs, openid_configs takes
// precedence as the proven path. When oidc_configs is set, new_flow_origins is
// cleared (and vice versa) since oidc_configs subsumes new_flow_origins.
Comment thread
sea-snake marked this conversation as resolved.
Outdated
if let Some(openid_configs) = arg.openid_configs {
if arg.oidc_configs.is_some() {
ic_cdk::println!(
"Both openid_configs and oidc_configs provided; using openid_configs"
);
}
state::persistent_state_mut(|persistent_state| {
persistent_state.new_flow_origins = Some(new_flow_origins);
persistent_state.openid_configs = Some(openid_configs);
persistent_state.oidc_configs = None;
})
} else if let Some(oidc_configs) = arg.oidc_configs {
state::persistent_state_mut(|persistent_state| {
persistent_state.oidc_configs = Some(oidc_configs);
persistent_state.openid_configs = None;
persistent_state.new_flow_origins = None;
})
}
if let Some(openid_configs) = arg.openid_configs {
if let Some(new_flow_origins) = arg.new_flow_origins {
state::persistent_state_mut(|persistent_state| {
persistent_state.openid_configs = Some(openid_configs);
persistent_state.new_flow_origins = Some(new_flow_origins);
persistent_state.oidc_configs = None;
})
}
if let Some(analytics_config) = arg.analytics_config {
Expand Down
50 changes: 47 additions & 3 deletions src/internet_identity/src/openid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ use internet_identity_interface::internet_identity::types::openid::{
OpenIdCredentialAddError, OpenIdDelegationError,
};
use internet_identity_interface::internet_identity::types::{
AnchorNumber, Delegation, IdRegFinishError, MetadataEntryV2, OpenIdConfig,
OpenIdEmailVerificationScheme, PublicKey, SessionKey, SignedDelegation, Timestamp, UserKey,
AnchorNumber, Delegation, DiscoverableOidcConfig, IdRegFinishError, MetadataEntryV2, OidcConfig,
OpenIdConfig, OpenIdEmailVerificationScheme, PublicKey, SessionKey, SignedDelegation, Timestamp,
UserKey,
};
use serde_bytes::ByteBuf;
use sha2::{Digest, Sha256};
Expand Down Expand Up @@ -255,6 +256,7 @@ struct PartialClaims {

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

pub fn setup(configs: Vec<OpenIdConfig>) {
Expand All @@ -265,6 +267,35 @@ pub fn setup(configs: Vec<OpenIdConfig>) {
});
}

pub fn setup_oidc(configs: Vec<DiscoverableOidcConfig>) {
OIDC_CONFIGS.with_borrow_mut(|stored| {
*stored = configs.clone();
});
PROVIDERS.with_borrow_mut(|providers| {
for config in configs {
providers.push(Box::new(generic::DiscoverableProvider::create(config)));
}
});
#[cfg(not(test))]
generic::init_discovery_timers();
}

pub fn get_discovered_oidc_configs() -> Vec<OidcConfig> {
OIDC_CONFIGS.with_borrow(|configs| {
configs
.iter()
.map(|config| OidcConfig {
name: config.name.clone(),
logo: config.logo.clone(),
discovery_url: config.discovery_url.clone(),
client_id: config.client_id.clone(),
issuer: generic::discovered_issuer_for(&config.discovery_url),
email_verification: config.email_verification,
})
.collect()
})
}

pub fn with_provider<F, R>(jwt: &str, callback: F) -> Result<R, OpenIDJWTVerificationError>
where
F: FnOnce(&dyn OpenIdProvider) -> Result<R, OpenIDJWTVerificationError>,
Expand All @@ -290,7 +321,20 @@ where
effective_issuer == iss
})
.ok_or_else(|| {
OpenIDJWTVerificationError::GenericError(format!("Unsupported issuer: {}", iss))
// Check if there are discoverable providers still initializing
// (their issuer is empty until discovery completes)
let has_pending = providers.iter().any(|p| p.issuer().is_empty());
if has_pending {
OpenIDJWTVerificationError::GenericError(
"OIDC provider discovery is still in progress, please try again shortly"
.to_string(),
)
} else {
OpenIDJWTVerificationError::GenericError(format!(
"Unsupported issuer: {}",
iss
))
}
})
.and_then(|provider| callback(provider.as_ref()))
})
Expand Down
Loading
Loading