mirror of
https://github.com/immich-app/immich.git
synced 2026-02-04 08:49:01 +03:00
refactor(dev): dockerify auth-server (#24377)
Description ----------- A while ago I asked on Discord if you people would be interested in removing incompatibilities with rootless docker. See: https://discord.com/channels/979116623879368755/1071165397228855327/1442974448776122592 The e2e tests in `e2e/src/api/specs/oauth.e2e-spec.ts` depend on a docker feature [host-gateway](https://docs.docker.com/reference/cli/dockerd/#configure-host-gateway-ip) that seemingly does not work on rootless docker. So the suggested change is to dockerify the `auth-server` and not run it on the docker host. I would love to receive feedback on this PR and feel free to request further improvements. Things that come to my mind: * Compile typescript instead of using `tsx` * Add hot-reloading of source files in `auth-server/` for development * Add `eslint` configuration for the new folder How Has This Been Tested? ------------------------ I'm running both default and rootless docker on my machine with [docker contexts](https://docs.docker.com/engine/manage-resources/contexts/): ``` docker context ls NAME DESCRIPTION DOCKER ENDPOINT ERROR default unix:///var/run/docker.sock rootless * unix:///run/user/1000/docker.sock ``` If I follow the steps from the [documentation](https://docs.immich.app/developer/testing) then `oauth.e2e-spec.ts` will fail because the `auth-server` on my host can't be reached. The tests pass after these steps: 1. `git switch refactor-auth-server-as-service` 2. `make e2e` 3. In another terminal `cd e2e` 4. `pnpm run test src/api/specs/oauth.e2e-spec.ts` passes Checklist: ---------- - [x] I have performed a self-review of my own code - [x] I have made corresponding changes to the documentation if applicable - [x] I have no unrelated changes in the PR. - [ ] I have confirmed that any new dependencies are strictly necessary. - [ ] I have written tests for new code (if applicable) - [ ] I have followed naming conventions/patterns in the surrounding code - [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc. - [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
This commit is contained in:
6
e2e-auth-server/Dockerfile
Normal file
6
e2e-auth-server/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25
|
||||
RUN corepack enable
|
||||
ADD package.json *.ts ./
|
||||
RUN pnpm install
|
||||
EXPOSE 2286
|
||||
CMD ["pnpm", "run", "start"]
|
||||
133
e2e-auth-server/auth-server.ts
Normal file
133
e2e-auth-server/auth-server.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { exportJWK, generateKeyPair } from 'jose';
|
||||
import Provider from 'oidc-provider';
|
||||
|
||||
export enum OAuthClient {
|
||||
DEFAULT = 'client-default',
|
||||
RS256_TOKENS = 'client-RS256-tokens',
|
||||
RS256_PROFILE = 'client-RS256-profile',
|
||||
}
|
||||
|
||||
export enum OAuthUser {
|
||||
NO_EMAIL = 'no-email',
|
||||
NO_NAME = 'no-name',
|
||||
WITH_QUOTA = 'with-quota',
|
||||
WITH_USERNAME = 'with-username',
|
||||
WITH_ROLE = 'with-role',
|
||||
}
|
||||
|
||||
const claims = [
|
||||
{ sub: OAuthUser.NO_EMAIL },
|
||||
{
|
||||
sub: OAuthUser.NO_NAME,
|
||||
email: 'oauth-no-name@immich.app',
|
||||
email_verified: true,
|
||||
},
|
||||
{
|
||||
sub: OAuthUser.WITH_USERNAME,
|
||||
email: 'oauth-with-username@immich.app',
|
||||
email_verified: true,
|
||||
immich_username: 'user-username',
|
||||
},
|
||||
{
|
||||
sub: OAuthUser.WITH_QUOTA,
|
||||
email: 'oauth-with-quota@immich.app',
|
||||
email_verified: true,
|
||||
preferred_username: 'user-quota',
|
||||
immich_quota: 25,
|
||||
},
|
||||
{
|
||||
sub: OAuthUser.WITH_ROLE,
|
||||
email: 'oauth-with-role@immich.app',
|
||||
email_verified: true,
|
||||
immich_role: 'admin',
|
||||
},
|
||||
];
|
||||
|
||||
const withDefaultClaims = (sub: string) => ({
|
||||
sub,
|
||||
email: `${sub}@immich.app`,
|
||||
name: 'OAuth User',
|
||||
given_name: `OAuth`,
|
||||
family_name: 'User',
|
||||
email_verified: true,
|
||||
});
|
||||
|
||||
const getClaims = (sub: string) => 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 port = 2286;
|
||||
const host = '0.0.0.0';
|
||||
const oidc = new Provider(`http://${host}:${port}`, {
|
||||
renderError: async (ctx, out, error) => {
|
||||
console.error(out);
|
||||
console.error(error);
|
||||
ctx.body = 'Internal Server Error';
|
||||
},
|
||||
findAccount: (ctx, sub) => ({ accountId: sub, claims: () => getClaims(sub) }),
|
||||
scopes: ['openid', 'email', 'profile'],
|
||||
claims: {
|
||||
openid: ['sub'],
|
||||
email: ['email', 'email_verified'],
|
||||
profile: [
|
||||
'name',
|
||||
'given_name',
|
||||
'family_name',
|
||||
'preferred_username',
|
||||
'immich_quota',
|
||||
'immich_username',
|
||||
'immich_role',
|
||||
],
|
||||
},
|
||||
features: {
|
||||
jwtUserinfo: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
cookies: {
|
||||
names: {
|
||||
session: 'oidc.session',
|
||||
interaction: 'oidc.interaction',
|
||||
resume: 'oidc.resume',
|
||||
state: 'oidc.state',
|
||||
},
|
||||
},
|
||||
pkce: {
|
||||
required: () => false,
|
||||
},
|
||||
jwks: { keys: [await exportJWK(privateKey)] },
|
||||
clients: [
|
||||
{
|
||||
client_id: OAuthClient.DEFAULT,
|
||||
client_secret: OAuthClient.DEFAULT,
|
||||
redirect_uris: redirectUris,
|
||||
grant_types: ['authorization_code'],
|
||||
response_types: ['code'],
|
||||
},
|
||||
{
|
||||
client_id: OAuthClient.RS256_TOKENS,
|
||||
client_secret: OAuthClient.RS256_TOKENS,
|
||||
redirect_uris: redirectUris,
|
||||
grant_types: ['authorization_code'],
|
||||
id_token_signed_response_alg: 'RS256',
|
||||
jwks: { keys: [await exportJWK(publicKey)] },
|
||||
},
|
||||
{
|
||||
client_id: OAuthClient.RS256_PROFILE,
|
||||
client_secret: OAuthClient.RS256_PROFILE,
|
||||
redirect_uris: redirectUris,
|
||||
grant_types: ['authorization_code'],
|
||||
userinfo_signed_response_alg: 'RS256',
|
||||
jwks: { keys: [await exportJWK(publicKey)] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const onStart = () => console.log(`[e2e-auth-server] http://${host}:${port}/.well-known/openid-configuration`);
|
||||
const app = oidc.listen(port, host, onStart);
|
||||
return () => app.close();
|
||||
};
|
||||
|
||||
export default setup;
|
||||
15
e2e-auth-server/package.json
Normal file
15
e2e-auth-server/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@immich/e2e-auth-server",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "auth-server.ts",
|
||||
"scripts": {
|
||||
"start": "tsx startup.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jose": "^5.6.3",
|
||||
"@types/oidc-provider": "^9.0.0",
|
||||
"oidc-provider": "^9.0.0",
|
||||
"tsx": "^4.20.6"
|
||||
}
|
||||
}
|
||||
8
e2e-auth-server/startup.ts
Normal file
8
e2e-auth-server/startup.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import setup from './auth-server'
|
||||
|
||||
const teardown = await setup()
|
||||
process.on('exit', () => {
|
||||
teardown()
|
||||
console.log('[e2e-auth-server] stopped')
|
||||
process.exit(0)
|
||||
})
|
||||
Reference in New Issue
Block a user