Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
668efd7
feat(be): add scoped session delegations
MRmarioruci Jun 12, 2026
2903333
feat(fe): use session delegations for ceremony-free account reads
MRmarioruci Jun 12, 2026
82ecccf
refactor(be,fe): drop epoch and scope from session delegations POC
MRmarioruci Jun 15, 2026
89cb8c8
refactor(be): move check_session_authorization into authz_utils
MRmarioruci Jun 15, 2026
5597fa2
refactor(be): lift session-authz gate above shared work in set_defaul…
MRmarioruci Jun 15, 2026
b5efc4a
Merge remote-tracking branch 'origin/main' into feat/session-delegati…
MRmarioruci Jun 15, 2026
23162dc
refactor(be): extract test fixtures for session delegation
MRmarioruci Jun 15, 2026
8de2a48
refactor(fe): rename session-delegation store exports for accuracy
MRmarioruci Jun 15, 2026
95423c4
refactor(be): tighten session-delegation scope to reads only
MRmarioruci Jun 16, 2026
0081ab8
fix(be): guard get_session_delegation against missing salt
MRmarioruci Jun 16, 2026
21782e5
fix(fe): guard handleEditAccount against missing account index
MRmarioruci Jun 16, 2026
6ab7f25
fix(fe): prefix unused identities fixture arg in session-delegation spec
MRmarioruci Jun 16, 2026
da2b600
feat(be): bump default session-delegation TTL to 30 days
MRmarioruci Jun 16, 2026
173f14f
docs(fe): explain EXPIRY_MARGIN_MS in session-delegation store
MRmarioruci Jun 16, 2026
2f418a4
test(be,fe): cover session-delegation expiry behavior
MRmarioruci Jun 16, 2026
98c7efb
Merge branch 'main' into feat/session-delegations-poc
MRmarioruci Jun 16, 2026
e7a6e49
Revert "test(be,fe): cover session-delegation expiry behavior"
MRmarioruci Jun 16, 2026
2c6ea10
refactor(be): drop email-recovery block in prepare_session_delegation
MRmarioruci Jun 17, 2026
3b0c2b5
feat(fe): persist multi-accounts toggle per anchor in localStorage
MRmarioruci Jun 17, 2026
f304d9e
fix(fe): stop multi-accounts toggle hydration effect from clobbering …
MRmarioruci Jun 17, 2026
953caf5
test(fe): fix account default-change test for persisted multi-account…
MRmarioruci Jun 17, 2026
f1f06e0
refactor(fe): mint session as authenticationStore side effect
MRmarioruci Jun 17, 2026
3452a82
fix(fe): gate session-delegation auth-store subscriber on \`browser\`
MRmarioruci Jun 17, 2026
d7ba077
fix(fe): revert auth-store subscriber, add explicit mintSession in au…
MRmarioruci Jun 17, 2026
a11d0af
feat(fe): wire mint/purge session into all remaining auth lifecycle s…
MRmarioruci Jun 18, 2026
54836ff
refactor(fe): mint session as a side effect of hooks.client.ts init
MRmarioruci Jun 18, 2026
3fb8959
Merge remote-tracking branch 'origin/main' into feat/session-delegati…
MRmarioruci Jun 18, 2026
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
73 changes: 73 additions & 0 deletions src/canister_tests/src/api/internet_identity/api_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,79 @@ pub fn get_accounts(
.map(|(x,)| x)
}

pub fn get_default_account(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
identity_number: IdentityNumber,
origin: FrontendHostname,
) -> Result<Result<AccountInfo, GetDefaultAccountError>, RejectResponse> {
query_candid_as(
env,
canister_id,
sender,
"get_default_account",
(identity_number, origin),
)
.map(|(x,)| x)
}

pub fn set_default_account(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
identity_number: IdentityNumber,
origin: FrontendHostname,
account_number: Option<AccountNumber>,
) -> Result<Result<AccountInfo, SetDefaultAccountError>, RejectResponse> {
call_candid_as(
env,
canister_id,
RawEffectivePrincipal::None,
sender,
"set_default_account",
(identity_number, origin, account_number),
)
.map(|(x,)| x)
}

pub fn prepare_session_delegation(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
anchor_number: AnchorNumber,
session_key: SessionKey,
max_ttl: Option<u64>,
) -> Result<Result<PrepareSessionDelegation, SessionDelegationError>, RejectResponse> {
call_candid_as(
env,
canister_id,
RawEffectivePrincipal::None,
sender,
"prepare_session_delegation",
(anchor_number, session_key, max_ttl),
)
.map(|(x,)| x)
}

pub fn get_session_delegation(
env: &PocketIc,
canister_id: CanisterId,
sender: Principal,
anchor_number: AnchorNumber,
session_key: SessionKey,
expiration: Timestamp,
) -> Result<Result<SignedDelegation, SessionDelegationError>, RejectResponse> {
query_candid_as(
env,
canister_id,
sender,
"get_session_delegation",
(anchor_number, session_key, expiration),
)
.map(|(x,)| x)
}

pub fn update_account(
env: &PocketIc,
canister_id: CanisterId,
Expand Down
14 changes: 14 additions & 0 deletions src/frontend/src/lib/flows/authFlow.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
} from "$lib/utils/openID";
import type { SsoDiscoveryResult } from "$lib/utils/ssoDiscovery";
import { nanosToMillis } from "$lib/utils/time";
import { mintSession } from "$lib/stores/session-delegation.store";

interface AuthFlowOptions {
trackLastUsed?: boolean;
Expand Down Expand Up @@ -342,6 +343,7 @@ export class AuthFlow {
});
const authMethod = { passkey: { credentialId } };
await authenticationStore.set({ identity, identityNumber, authMethod });
void mintSession({ identityNumber, actor: get(authenticatedStore).actor });
const info =
await get(authenticatedStore).actor.get_anchor_info(identityNumber);
const pendingLastUsedEntry = this.#options.trackLastUsed
Expand Down Expand Up @@ -531,6 +533,10 @@ export class AuthFlow {
identityNumber,
authMethod: { openid: { iss, sub } },
});
void mintSession({
identityNumber,
actor: get(authenticatedStore).actor,
});
const info =
await get(authenticatedStore).actor.get_anchor_info(identityNumber);
return {
Expand Down Expand Up @@ -626,6 +632,10 @@ export class AuthFlow {
identityNumber,
authMethod: { passkey: { credentialId } },
});
void mintSession({
identityNumber,
actor: get(authenticatedStore).actor,
});
if (this.#options.trackLastUsed) {
lastUsedIdentitiesStore.addLastUsedIdentity({
identityNumber,
Expand Down Expand Up @@ -791,6 +801,10 @@ export class AuthFlow {
identityNumber,
authMethod: { openid: { iss, sub } },
});
void mintSession({
identityNumber,
actor: get(authenticatedStore).actor,
});
this.#captcha = undefined;
return { iss, sub, loginHint, identityNumber, decodedJwt };
} catch (error) {
Expand Down
29 changes: 29 additions & 0 deletions src/frontend/src/lib/generated/internet_identity_idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,11 @@ export const idlFactory = ({ IDL }) => {
'Unauthorized' : IDL.Principal,
'NoSuchCredentials' : IDL.Text,
});
const SessionDelegationError = IDL.Variant({
'NoSuchDelegation' : IDL.Null,
'InternalCanisterError' : IDL.Text,
'Unauthorized' : IDL.Principal,
});
const HeaderField = IDL.Tuple(IDL.Text, IDL.Text);
const HttpRequest = IDL.Record({
'url' : IDL.Text,
Expand Down Expand Up @@ -693,6 +698,10 @@ export const idlFactory = ({ IDL }) => {
'InternalCanisterError' : IDL.Text,
'Unauthorized' : IDL.Principal,
});
const PrepareSessionDelegation = IDL.Record({
'user_key' : UserKey,
'expiration' : Timestamp,
});
const ChallengeResult = IDL.Record({
'key' : ChallengeKey,
'chars' : IDL.Text,
Expand Down Expand Up @@ -1006,6 +1015,16 @@ export const idlFactory = ({ IDL }) => {
[IDL.Principal],
['query'],
),
'get_session_delegation' : IDL.Func(
[UserNumber, SessionKey, Timestamp],
[
IDL.Variant({
'Ok' : SignedDelegation,
'Err' : SessionDelegationError,
}),
],
['query'],
),
'http_request' : IDL.Func([HttpRequest], [HttpResponse], ['query']),
'identity_authn_info' : IDL.Func(
[IdentityNumber],
Expand Down Expand Up @@ -1155,6 +1174,16 @@ export const idlFactory = ({ IDL }) => {
[IDL.Variant({ 'Ok' : PreparedIdAlias, 'Err' : PrepareIdAliasError })],
[],
),
'prepare_session_delegation' : IDL.Func(
[UserNumber, SessionKey, IDL.Opt(IDL.Nat64)],
[
IDL.Variant({
'Ok' : PrepareSessionDelegation,
'Err' : SessionDelegationError,
}),
],
[],
),
'register' : IDL.Func(
[DeviceData, ChallengeResult, IDL.Opt(IDL.Principal)],
[RegisterResponse],
Expand Down
17 changes: 17 additions & 0 deletions src/frontend/src/lib/generated/internet_identity_types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,10 @@ export interface PrepareIdAliasRequest {
*/
'identity_number' : IdentityNumber,
}
export interface PrepareSessionDelegation {
'user_key' : UserKey,
'expiration' : Timestamp,
}
/**
* The prepared id alias contains two (still unsigned) credentials in JWT format,
* certifying the id alias for the issuer resp. the relying party.
Expand Down Expand Up @@ -1394,6 +1398,9 @@ export interface Rrsig {
'type_covered' : number,
}
export type Salt = Uint8Array | number[];
export type SessionDelegationError = { 'NoSuchDelegation' : null } |
{ 'InternalCanisterError' : string } |
{ 'Unauthorized' : Principal };
export type SessionKey = PublicKey;
export type SetDefaultAccountError = {
'NoSuchOrigin' : { 'anchor_number' : UserNumber }
Expand Down Expand Up @@ -1822,6 +1829,11 @@ export interface _SERVICE {
{ 'Err' : GetIdAliasError }
>,
'get_principal' : ActorMethod<[UserNumber, FrontendHostname], Principal>,
'get_session_delegation' : ActorMethod<
[UserNumber, SessionKey, Timestamp],
{ 'Ok' : SignedDelegation } |
{ 'Err' : SessionDelegationError }
>,
/**
* HTTP Gateway protocol
* =====================
Expand Down Expand Up @@ -1994,6 +2006,11 @@ export interface _SERVICE {
{ 'Ok' : PreparedIdAlias } |
{ 'Err' : PrepareIdAliasError }
>,
'prepare_session_delegation' : ActorMethod<
[UserNumber, SessionKey, [] | [bigint]],
{ 'Ok' : PrepareSessionDelegation } |
{ 'Err' : SessionDelegationError }
>,
'register' : ActorMethod<
[DeviceData, ChallengeResult, [] | [Principal]],
RegisterResponse
Expand Down
Loading
Loading