Skip to content

Commit 476d693

Browse files
committed
Allow account linking via a code
--- Adds a feature to create account linking codes. These will be used with native apps to link existing accounts to the passkeys created on these devices.
1 parent 774bc9a commit 476d693

16 files changed

+321
-1
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use candid::{CandidType, Deserialize};
2+
use serde::Serialize;
3+
use ts_export::ts_export;
4+
use types::{AccountLinkingCode, Empty};
5+
6+
pub type Args = Empty;
7+
8+
#[ts_export(identity, get_account_linking_code)]
9+
#[derive(CandidType, Serialize, Deserialize, Debug)]
10+
pub enum Response {
11+
Success(AccountLinkingCode),
12+
NotFound,
13+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
pub mod auth_principals;
22
pub mod check_auth_principal_v2;
3+
pub mod get_account_linking_code;
34
pub mod get_delegation;
45
pub mod lookup_webauthn_pubkey;
6+
pub mod verify_account_linking_code;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use candid::{CandidType, Deserialize};
2+
use serde::Serialize;
3+
use ts_export::ts_export;
4+
5+
#[ts_export(identity, verify_account_linking_code)]
6+
#[derive(CandidType, Serialize, Deserialize, Debug)]
7+
pub struct Args {
8+
pub code: String,
9+
}
10+
11+
#[ts_export(notifications_index, fcm_token_exists)]
12+
#[derive(CandidType, Serialize, Deserialize, Debug)]
13+
pub struct Response(pub bool);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
use candid::{CandidType, Deserialize};
2+
use serde::Serialize;
3+
use ts_export::ts_export;
4+
use types::{AccountLinkingCode, Empty};
5+
6+
pub type Args = Empty;
7+
8+
#[ts_export(identity, create_identity)]
9+
#[derive(CandidType, Serialize, Deserialize, Debug)]
10+
pub enum Response {
11+
Success(AccountLinkingCode),
12+
UserNotFound,
13+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use crate::WebAuthnKey;
2+
use candid::{CandidType, Deserialize};
3+
use serde::Serialize;
4+
use ts_export::ts_export;
5+
use types::CanisterId;
6+
7+
#[ts_export(identity, use_account_linking_code)]
8+
#[derive(CandidType, Serialize, Deserialize, Debug)]
9+
pub struct Args {
10+
pub code: String,
11+
#[serde(with = "serde_bytes")]
12+
pub public_key: Vec<u8>,
13+
pub webauthn_key: Option<WebAuthnKey>,
14+
}
15+
16+
#[ts_export(identity, use_account_linking_code)]
17+
#[derive(CandidType, Serialize, Deserialize, Debug)]
18+
pub enum Response {
19+
Success,
20+
AlreadyRegistered,
21+
AlreadyLinkedToPrincipal,
22+
TargetUserNotFound,
23+
PublicKeyInvalid(String),
24+
OriginatingCanisterInvalid(CanisterId),
25+
LinkedIdentitiesLimitReached(u32),
26+
LinkingCodeNotFound,
27+
LinkingCodeExpired,
28+
LinkingCodeInvalid,
29+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
pub mod accept_identity_link_via_qr_code;
22
pub mod approve_identity_link;
33
pub mod c2c_set_user_ids;
4+
pub mod create_account_linking_code;
45
pub mod create_identity;
56
pub mod delete_user;
67
pub mod generate_challenge;
78
pub mod get_encryption_key;
89
pub mod initiate_identity_link;
910
pub mod initiate_identity_link_via_qr_code;
11+
pub mod link_with_account_linking_code;
1012
pub mod prepare_delegation;
1113
pub mod remove_identity_link;

backend/canisters/identity/impl/src/lib.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ use serde_bytes::ByteBuf;
2020
use sha256::sha256;
2121
use std::cell::RefCell;
2222
use std::collections::{BTreeMap, HashMap, HashSet};
23-
use types::{BuildVersion, CanisterId, Cycles, Milliseconds, OCResult, TimestampMillis, Timestamped};
23+
use types::AccountLinkingCode;
24+
use types::{BuildVersion, CanisterId, Cycles, Milliseconds, OCResult, TimestampMillis, Timestamped, UserId};
2425
use utils::env::Environment;
2526
use x509_parser::prelude::{FromDer, SubjectPublicKeyInfo};
2627

@@ -142,6 +143,14 @@ impl RuntimeState {
142143
}
143144
}
144145

146+
pub fn get_user_id_by_caller(&self) -> Option<UserId> {
147+
let caller = self.env.caller();
148+
self.data
149+
.user_principals
150+
.get_by_auth_principal(&caller)
151+
.and_then(|u| u.user_id)
152+
}
153+
145154
pub fn metrics(&self) -> Metrics {
146155
Metrics {
147156
heap_memory_used: utils::memory::heap(),
@@ -184,6 +193,8 @@ struct Data {
184193
rng_seed: [u8; 32],
185194
challenges: Challenges,
186195
test_mode: bool,
196+
#[serde(default)]
197+
account_linking_codes: HashMap<String, (UserId, AccountLinkingCode)>,
187198
}
188199

189200
impl Data {
@@ -213,6 +224,7 @@ impl Data {
213224
rng_seed: [0; 32],
214225
challenges: Challenges::default(),
215226
test_mode,
227+
account_linking_codes: HashMap::new(),
216228
}
217229
}
218230

backend/canisters/identity/impl/src/model/user_principals.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@ impl UserPrincipals {
224224
}
225225
}
226226

227+
pub fn find_user_principal_by_user_id(&self, user_id: UserId) -> Option<(Principal, Vec<Principal>)> {
228+
self.user_principals
229+
.iter()
230+
.find(|u| u.user_id == Some(user_id))
231+
.map(|u| (u.principal, u.auth_principals.clone()))
232+
}
233+
227234
// This is O(number of users) so only use in rare cases
228235
pub fn get_originating_canisters_by_user_id_slow(&self, user_id: UserId) -> Vec<(CanisterId, bool)> {
229236
if let Some(user_principal) = self.user_principals.iter().find(|u| u.user_id == Some(user_id)) {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use crate::{RuntimeState, read_state};
2+
use canister_api_macros::query;
3+
use identity_canister::get_account_linking_code::{Response::*, *};
4+
use types::{AccountLinkingCode, UserId};
5+
6+
#[query(msgpack = true, candid = true)]
7+
fn get_account_linking_code(_args: Args) -> Response {
8+
read_state(get_account_linking_code_impl)
9+
}
10+
11+
// Fetches existing account linking code for the caller's user ID.
12+
fn get_account_linking_code_impl(state: &RuntimeState) -> Response {
13+
let now = state.env.now();
14+
15+
if let Some(user_id) = state.get_user_id_by_caller() {
16+
if let Some(alc) = find_account_linking_code_for_user_id(state, &user_id) {
17+
if alc.is_valid(now) { Success(alc.clone()) } else { NotFound }
18+
} else {
19+
NotFound
20+
}
21+
} else {
22+
NotFound
23+
}
24+
}
25+
26+
fn find_account_linking_code_for_user_id(state: &RuntimeState, user_id: &UserId) -> Option<AccountLinkingCode> {
27+
state
28+
.data
29+
.account_linking_codes
30+
.iter()
31+
.find(|(_, (alc_user_id, _))| alc_user_id == user_id)
32+
.map(|(_, (_, alc))| alc.clone())
33+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod auth_principals;
22
mod check_auth_principal_v2;
3+
mod get_account_linking_code;
34
mod get_delegation;
45
mod http_request;
56
mod lookup_webauthn_pubkey;
7+
mod verify_account_linking_code;

0 commit comments

Comments
 (0)