Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/check-pr-template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
OK=true
while IFS= read -r header; do
printf '%s\n' "$BODY" | grep -qF "$header" || OK=false
done < <(grep "^## " .github/pull_request_template.md)
done < <(sed '/<!--/,/-->/d' .github/pull_request_template.md | grep "^## ")
echo "uses_template=$OK" >> "$GITHUB_OUTPUT"

act:
Expand Down
29 changes: 25 additions & 4 deletions e2e-auth-server/auth-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum OAuthClient {
export enum OAuthUser {
NO_EMAIL = 'no-email',
NO_NAME = 'no-name',
ID_TOKEN_CLAIMS = 'id-token-claims',
WITH_QUOTA = 'with-quota',
WITH_USERNAME = 'with-username',
WITH_ROLE = 'with-role',
Expand Down Expand Up @@ -52,12 +53,25 @@ const withDefaultClaims = (sub: string) => ({
email_verified: true,
});

const getClaims = (sub: string) => claims.find((user) => user.sub === sub) || withDefaultClaims(sub);
const getClaims = (sub: string, use?: string) => {
if (sub === OAuthUser.ID_TOKEN_CLAIMS) {
return {
sub,
email: `oauth-${sub}@immich.app`,
email_verified: true,
name: use === 'id_token' ? 'ID Token User' : 'Userinfo User',
};
}
return claims.find((user) => user.sub === sub) || withDefaultClaims(sub);
};

const setup = async () => {
const { privateKey, publicKey } = await generateKeyPair('RS256');

const redirectUris = ['http://127.0.0.1:2285/auth/login', 'https://photos.immich.app/oauth/mobile-redirect'];
const redirectUris = [
'http://127.0.0.1:2285/auth/login',
'https://photos.immich.app/oauth/mobile-redirect',
];
const port = 2286;
const host = '0.0.0.0';
const oidc = new Provider(`http://${host}:${port}`, {
Expand All @@ -66,7 +80,10 @@ const setup = async () => {
console.error(error);
ctx.body = 'Internal Server Error';
},
findAccount: (ctx, sub) => ({ accountId: sub, claims: () => getClaims(sub) }),
findAccount: (ctx, sub) => ({
accountId: sub,
claims: (use) => getClaims(sub, use),
}),
scopes: ['openid', 'email', 'profile'],
claims: {
openid: ['sub'],
Expand Down Expand Up @@ -94,6 +111,7 @@ const setup = async () => {
state: 'oidc.state',
},
},
conformIdTokenClaims: false,
pkce: {
required: () => false,
},
Expand Down Expand Up @@ -125,7 +143,10 @@ const setup = async () => {
],
});

const onStart = () => console.log(`[e2e-auth-server] http://${host}:${port}/.well-known/openid-configuration`);
const onStart = () =>
console.log(
`[e2e-auth-server] http://${host}:${port}/.well-known/openid-configuration`,
);
const app = oidc.listen(port, host, onStart);
return () => app.close();
};
Expand Down
19 changes: 19 additions & 0 deletions e2e/src/specs/server/api/oauth.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,4 +380,23 @@ describe(`/oauth`, () => {
});
});
});

describe('idTokenClaims', () => {
it('should use claims from the ID token if IDP includes them', async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
});
const callbackParams = await loginWithOAuth(OAuthUser.ID_TOKEN_CLAIMS);
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(201);
expect(body).toMatchObject({
accessToken: expect.any(String),
name: 'ID Token User',
userEmail: 'oauth-id-token-claims@immich.app',
userId: expect.any(String),
});
});
});
});
11 changes: 10 additions & 1 deletion server/src/repositories/oauth.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,16 @@ export class OAuthRepository {

try {
const tokens = await authorizationCodeGrant(client, new URL(url), { expectedState, pkceCodeVerifier });
const profile = await fetchUserInfo(client, tokens.access_token, oidc.skipSubjectCheck);

let profile: OAuthProfile;
const tokenClaims = tokens.claims();
if (tokenClaims && 'email' in tokenClaims) {
this.logger.debug('Using ID token claims instead of userinfo endpoint');
profile = tokenClaims as OAuthProfile;
} else {
profile = await fetchUserInfo(client, tokens.access_token, oidc.skipSubjectCheck);
}

if (!profile.sub) {
throw new Error('Unexpected profile response, no `sub`');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class MonthGroup {
this.#initialCount = initialCount;
this.#sortOrder = order;

this.yearMonth = yearMonth;
this.yearMonth = { year: yearMonth.year, month: yearMonth.month };
this.monthGroupTitle = formatMonthGroupTitle(fromTimelinePlainYearMonth(yearMonth));

this.loader = new CancellableTask(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,29 @@ describe('TimelineManager', () => {
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })).not.toBeUndefined();
expect(getMonthGroupByDate(timelineManager, { year: 2024, month: 3 })?.getAssets().length).toEqual(1);
});

it('yearMonth is not a shared reference with asset.localDateTime (reference bug)', () => {
const asset = deriveLocalDateTimeFromFileCreatedAt(
timelineAssetFactory.build({
fileCreatedAt: fromISODateTimeUTCToObject('2024-01-20T12:00:00.000Z'),
}),
);

timelineManager.upsertAssets([asset]);
const januaryMonth = getMonthGroupByDate(timelineManager, { year: 2024, month: 1 })!;
const monthYearMonth = januaryMonth.yearMonth;

const originalMonth = monthYearMonth.month;
expect(originalMonth).toEqual(1);

// Simulating updateObject
asset.localDateTime.month = 3;
asset.localDateTime.day = 20;

expect(monthYearMonth.month).toEqual(originalMonth);
expect(monthYearMonth.month).toEqual(1);
});

it('asset is removed during upsert when TimelineManager if visibility changes', async () => {
await timelineManager.updateOptions({
visibility: AssetVisibility.Archive,
Expand Down
Loading