mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
refactor(server): cookies (#8920)
This commit is contained in:
parent
eaf9e5e477
commit
74c921148b
@ -112,9 +112,29 @@ describe('/auth/*', () => {
|
||||
|
||||
const cookies = headers['set-cookie'];
|
||||
expect(cookies).toHaveLength(3);
|
||||
expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`);
|
||||
expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;');
|
||||
expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;');
|
||||
expect(cookies[0].split(';').map((item) => item.trim())).toEqual([
|
||||
`immich_access_token=${token}`,
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'HttpOnly',
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
expect(cookies[1].split(';').map((item) => item.trim())).toEqual([
|
||||
'immich_auth_type=password',
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'HttpOnly',
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
expect(cookies[2].split(';').map((item) => item.trim())).toEqual([
|
||||
'immich_is_authenticated=true',
|
||||
'Max-Age=34560000',
|
||||
'Path=/',
|
||||
expect.stringContaining('Expires='),
|
||||
'SameSite=Lax',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -26,12 +26,7 @@ export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile);
|
||||
|
||||
export const MOBILE_REDIRECT = 'app.immich:/';
|
||||
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||
export const IMMICH_ACCESS_COOKIE = 'immich_access_token';
|
||||
export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated';
|
||||
export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type';
|
||||
export const IMMICH_API_KEY_NAME = 'api_key';
|
||||
export const IMMICH_API_KEY_HEADER = 'x-api-key';
|
||||
export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token';
|
||||
|
||||
export enum AuthType {
|
||||
PASSWORD = 'password',
|
||||
OAUTH = 'oauth',
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Request, Response } from 'express';
|
||||
import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants';
|
||||
import { AuthType } from 'src/constants';
|
||||
import {
|
||||
AuthDto,
|
||||
ChangePasswordDto,
|
||||
ImmichCookie,
|
||||
LoginCredentialDto,
|
||||
LoginResponseDto,
|
||||
LogoutResponseDto,
|
||||
@ -14,6 +15,7 @@ import {
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||
import { respondWithCookie, respondWithoutCookie } from 'src/utils/response';
|
||||
|
||||
@ApiTags('Authentication')
|
||||
@Controller('auth')
|
||||
@ -28,9 +30,15 @@ export class AuthController {
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@GetLoginDetails() loginDetails: LoginDetails,
|
||||
): Promise<LoginResponseDto> {
|
||||
const { response, cookie } = await this.service.login(loginCredential, loginDetails);
|
||||
res.header('Set-Cookie', cookie);
|
||||
return response;
|
||||
const body = await this.service.login(loginCredential, loginDetails);
|
||||
return respondWithCookie(res, body, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [
|
||||
{ key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken },
|
||||
{ key: ImmichCookie.AUTH_TYPE, value: AuthType.PASSWORD },
|
||||
{ key: ImmichCookie.IS_AUTHENTICATED, value: 'true' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@PublicRoute()
|
||||
@ -53,15 +61,18 @@ export class AuthController {
|
||||
|
||||
@Post('logout')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
logout(
|
||||
async logout(
|
||||
@Req() request: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@Auth() auth: AuthDto,
|
||||
): Promise<LogoutResponseDto> {
|
||||
res.clearCookie(IMMICH_ACCESS_COOKIE);
|
||||
res.clearCookie(IMMICH_AUTH_TYPE_COOKIE);
|
||||
res.clearCookie(IMMICH_IS_AUTHENTICATED);
|
||||
const authType = (request.cookies || {})[ImmichCookie.AUTH_TYPE];
|
||||
|
||||
return this.service.logout(auth, (request.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]);
|
||||
const body = await this.service.logout(auth, authType);
|
||||
return respondWithoutCookie(res, body, [
|
||||
ImmichCookie.ACCESS_TOKEN,
|
||||
ImmichCookie.AUTH_TYPE,
|
||||
ImmichCookie.IS_AUTHENTICATED,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { Body, Controller, Get, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Request, Response } from 'express';
|
||||
import { AuthType } from 'src/constants';
|
||||
import {
|
||||
AuthDto,
|
||||
ImmichCookie,
|
||||
LoginResponseDto,
|
||||
OAuthAuthorizeResponseDto,
|
||||
OAuthCallbackDto,
|
||||
@ -11,6 +13,7 @@ import {
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
|
||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||
import { respondWithCookie } from 'src/utils/response';
|
||||
|
||||
@ApiTags('OAuth')
|
||||
@Controller('oauth')
|
||||
@ -41,9 +44,15 @@ export class OAuthController {
|
||||
@Body() dto: OAuthCallbackDto,
|
||||
@GetLoginDetails() loginDetails: LoginDetails,
|
||||
): Promise<LoginResponseDto> {
|
||||
const { response, cookie } = await this.service.callback(dto, loginDetails);
|
||||
res.header('Set-Cookie', cookie);
|
||||
return response;
|
||||
const body = await this.service.callback(dto, loginDetails);
|
||||
return respondWithCookie(res, body, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [
|
||||
{ key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken },
|
||||
{ key: ImmichCookie.AUTH_TYPE, value: AuthType.OAUTH },
|
||||
{ key: ImmichCookie.IS_AUTHENTICATED, value: 'true' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@Post('link')
|
||||
|
@ -1,18 +1,19 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Request, Response } from 'express';
|
||||
import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/constants';
|
||||
import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AuthDto, ImmichCookie } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
SharedLinkCreateDto,
|
||||
SharedLinkEditDto,
|
||||
SharedLinkPasswordDto,
|
||||
SharedLinkResponseDto,
|
||||
} from 'src/dtos/shared-link.dto';
|
||||
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||
import { Auth, Authenticated, GetLoginDetails, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||
import { LoginDetails } from 'src/services/auth.service';
|
||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||
import { respondWithCookie } from 'src/utils/response';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Shared Link')
|
||||
@ -33,20 +34,17 @@ export class SharedLinkController {
|
||||
@Query() dto: SharedLinkPasswordDto,
|
||||
@Req() request: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@GetLoginDetails() loginDetails: LoginDetails,
|
||||
): Promise<SharedLinkResponseDto> {
|
||||
const sharedLinkToken = request.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE];
|
||||
const sharedLinkToken = request.cookies?.[ImmichCookie.SHARED_LINK_TOKEN];
|
||||
if (sharedLinkToken) {
|
||||
dto.token = sharedLinkToken;
|
||||
}
|
||||
const response = await this.service.getMine(auth, dto);
|
||||
if (response.token) {
|
||||
res.cookie(IMMICH_SHARED_LINK_ACCESS_COOKIE, response.token, {
|
||||
expires: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
}
|
||||
return response;
|
||||
const body = await this.service.getMine(auth, dto);
|
||||
return respondWithCookie(res, body, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: body.token ? [{ key: ImmichCookie.SHARED_LINK_TOKEN, value: body.token }] : [],
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
@ -6,6 +6,25 @@ import { SessionEntity } from 'src/entities/session.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export enum ImmichCookie {
|
||||
ACCESS_TOKEN = 'immich_access_token',
|
||||
AUTH_TYPE = 'immich_auth_type',
|
||||
IS_AUTHENTICATED = 'immich_is_authenticated',
|
||||
SHARED_LINK_TOKEN = 'immich_shared_link_token',
|
||||
}
|
||||
|
||||
export enum ImmichHeader {
|
||||
API_KEY = 'x-api-key',
|
||||
USER_TOKEN = 'x-immich-user-token',
|
||||
SESSION_TOKEN = 'x-immich-session-token',
|
||||
SHARED_LINK_TOKEN = 'x-immich-share-key',
|
||||
}
|
||||
|
||||
export type CookieResponse = {
|
||||
isSecure: boolean;
|
||||
values: Array<{ key: ImmichCookie; value: string }>;
|
||||
};
|
||||
|
||||
export class AuthDto {
|
||||
user!: UserEntity;
|
||||
|
||||
@ -39,7 +58,7 @@ export class LoginResponseDto {
|
||||
|
||||
export function mapLoginResponse(entity: UserEntity, accessToken: string): LoginResponseDto {
|
||||
return {
|
||||
accessToken: accessToken,
|
||||
accessToken,
|
||||
userId: entity.id,
|
||||
userEmail: entity.email,
|
||||
name: entity.name,
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { IMMICH_API_KEY_NAME } from 'src/constants';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { AuthService, LoginDetails } from 'src/services/auth.service';
|
||||
@ -21,6 +20,7 @@ export enum Metadata {
|
||||
ADMIN_ROUTE = 'admin_route',
|
||||
SHARED_ROUTE = 'shared_route',
|
||||
PUBLIC_SECURITY = 'public_security',
|
||||
API_KEY_SECURITY = 'api_key',
|
||||
}
|
||||
|
||||
export interface AuthenticatedOptions {
|
||||
@ -32,7 +32,7 @@ export const Authenticated = (options: AuthenticatedOptions = {}) => {
|
||||
const decorators: MethodDecorator[] = [
|
||||
ApiBearerAuth(),
|
||||
ApiCookieAuth(),
|
||||
ApiSecurity(IMMICH_API_KEY_NAME),
|
||||
ApiSecurity(Metadata.API_KEY_SECURITY),
|
||||
SetMetadata(Metadata.AUTH_ROUTE, true),
|
||||
];
|
||||
|
||||
|
@ -143,20 +143,6 @@ describe('AuthService', () => {
|
||||
await expect(sut.login(fixtures.login, loginDetails)).resolves.toEqual(loginResponseStub.user1password);
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should generate the cookie headers (insecure)', async () => {
|
||||
userMock.getByEmail.mockResolvedValue(userStub.user1);
|
||||
sessionMock.create.mockResolvedValue(sessionStub.valid);
|
||||
await expect(
|
||||
sut.login(fixtures.login, {
|
||||
clientIp: '127.0.0.1',
|
||||
isSecure: false,
|
||||
deviceOS: '',
|
||||
deviceType: '',
|
||||
}),
|
||||
).resolves.toEqual(loginResponseStub.user1insecure);
|
||||
expect(userMock.getByEmail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changePassword', () => {
|
||||
|
@ -10,23 +10,16 @@ import cookieParser from 'cookie';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IncomingHttpHeaders } from 'node:http';
|
||||
import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
||||
import {
|
||||
AuthType,
|
||||
IMMICH_ACCESS_COOKIE,
|
||||
IMMICH_API_KEY_HEADER,
|
||||
IMMICH_AUTH_TYPE_COOKIE,
|
||||
IMMICH_IS_AUTHENTICATED,
|
||||
LOGIN_URL,
|
||||
MOBILE_REDIRECT,
|
||||
} from 'src/constants';
|
||||
import { AuthType, LOGIN_URL, MOBILE_REDIRECT } from 'src/constants';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { UserCore } from 'src/cores/user.core';
|
||||
import {
|
||||
AuthDto,
|
||||
ChangePasswordDto,
|
||||
ImmichCookie,
|
||||
ImmichHeader,
|
||||
LoginCredentialDto,
|
||||
LoginResponseDto,
|
||||
LogoutResponseDto,
|
||||
OAuthAuthorizeResponseDto,
|
||||
OAuthCallbackDto,
|
||||
@ -55,11 +48,6 @@ export interface LoginDetails {
|
||||
deviceOS: string;
|
||||
}
|
||||
|
||||
interface LoginResponse {
|
||||
response: LoginResponseDto;
|
||||
cookie: string[];
|
||||
}
|
||||
|
||||
interface OAuthProfile extends UserinfoResponse {
|
||||
email: string;
|
||||
}
|
||||
@ -95,7 +83,7 @@ export class AuthService {
|
||||
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
async login(dto: LoginCredentialDto, details: LoginDetails): Promise<LoginResponse> {
|
||||
async login(dto: LoginCredentialDto, details: LoginDetails) {
|
||||
const config = await this.configCore.getConfig();
|
||||
if (!config.passwordLogin.enabled) {
|
||||
throw new UnauthorizedException('Password login has been disabled');
|
||||
@ -114,7 +102,7 @@ export class AuthService {
|
||||
throw new UnauthorizedException('Incorrect email or password');
|
||||
}
|
||||
|
||||
return this.createLoginResponse(user, AuthType.PASSWORD, details);
|
||||
return this.createLoginResponse(user, details);
|
||||
}
|
||||
|
||||
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
|
||||
@ -161,13 +149,13 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async validate(headers: IncomingHttpHeaders, params: Record<string, string>): Promise<AuthDto> {
|
||||
const shareKey = (headers['x-immich-share-key'] || params.key) as string;
|
||||
const session = (headers['x-immich-user-token'] ||
|
||||
headers['x-immich-session-token'] ||
|
||||
const shareKey = (headers[ImmichHeader.SHARED_LINK_TOKEN] || params.key) as string;
|
||||
const session = (headers[ImmichHeader.USER_TOKEN] ||
|
||||
headers[ImmichHeader.SESSION_TOKEN] ||
|
||||
params.sessionKey ||
|
||||
this.getBearerToken(headers) ||
|
||||
this.getCookieToken(headers)) as string;
|
||||
const apiKey = (headers[IMMICH_API_KEY_HEADER] || params.apiKey) as string;
|
||||
const apiKey = (headers[ImmichHeader.API_KEY] || params.apiKey) as string;
|
||||
|
||||
if (shareKey) {
|
||||
return this.validateSharedLink(shareKey);
|
||||
@ -204,10 +192,7 @@ export class AuthService {
|
||||
return { url };
|
||||
}
|
||||
|
||||
async callback(
|
||||
dto: OAuthCallbackDto,
|
||||
loginDetails: LoginDetails,
|
||||
): Promise<{ response: LoginResponseDto; cookie: string[] }> {
|
||||
async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) {
|
||||
const config = await this.configCore.getConfig();
|
||||
const profile = await this.getOAuthProfile(config, dto.url);
|
||||
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
||||
@ -256,7 +241,7 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
return this.createLoginResponse(user, AuthType.OAUTH, loginDetails);
|
||||
return this.createLoginResponse(user, loginDetails);
|
||||
}
|
||||
|
||||
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserResponseDto> {
|
||||
@ -353,7 +338,7 @@ export class AuthService {
|
||||
|
||||
private getCookieToken(headers: IncomingHttpHeaders): string | null {
|
||||
const cookies = cookieParser.parse(headers.cookie || '');
|
||||
return cookies[IMMICH_ACCESS_COOKIE] || null;
|
||||
return cookies[ImmichCookie.ACCESS_TOKEN] || null;
|
||||
}
|
||||
|
||||
async validateSharedLink(key: string | string[]): Promise<AuthDto> {
|
||||
@ -405,7 +390,7 @@ export class AuthService {
|
||||
throw new UnauthorizedException('Invalid user token');
|
||||
}
|
||||
|
||||
private async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) {
|
||||
private async createLoginResponse(user: UserEntity, loginDetails: LoginDetails) {
|
||||
const key = this.cryptoRepository.newPassword(32);
|
||||
const token = this.cryptoRepository.hashSha256(key);
|
||||
|
||||
@ -416,28 +401,7 @@ export class AuthService {
|
||||
deviceType: loginDetails.deviceType,
|
||||
});
|
||||
|
||||
const response = mapLoginResponse(user, key);
|
||||
const cookie = this.getCookies(response, authType, loginDetails);
|
||||
return { response, cookie };
|
||||
}
|
||||
|
||||
private getCookies(loginResponse: LoginResponseDto, authType: AuthType, { isSecure }: LoginDetails) {
|
||||
const maxAge = 400 * 24 * 3600; // 400 days
|
||||
|
||||
let authTypeCookie = '';
|
||||
let accessTokenCookie = '';
|
||||
let isAuthenticatedCookie = '';
|
||||
|
||||
if (isSecure) {
|
||||
accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||
authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||
isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Secure; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||
} else {
|
||||
accessTokenCookie = `${IMMICH_ACCESS_COOKIE}=${loginResponse.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||
authTypeCookie = `${IMMICH_AUTH_TYPE_COOKIE}=${authType}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||
isAuthenticatedCookie = `${IMMICH_IS_AUTHENTICATED}=true; Path=/; Max-Age=${maxAge}; SameSite=Lax;`;
|
||||
}
|
||||
return [accessTokenCookie, authTypeCookie, isAuthenticatedCookie];
|
||||
return mapLoginResponse(user, key);
|
||||
}
|
||||
|
||||
private getClaim<T>(profile: OAuthProfile, options: ClaimOptions<T>): T {
|
||||
|
@ -10,13 +10,8 @@ import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.inte
|
||||
import _ from 'lodash';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
CLIP_MODEL_INFO,
|
||||
IMMICH_ACCESS_COOKIE,
|
||||
IMMICH_API_KEY_HEADER,
|
||||
IMMICH_API_KEY_NAME,
|
||||
serverVersion,
|
||||
} from 'src/constants';
|
||||
import { CLIP_MODEL_INFO, serverVersion } from 'src/constants';
|
||||
import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { Metadata } from 'src/middleware/auth.guard';
|
||||
|
||||
@ -143,14 +138,14 @@ export const useSwagger = (app: INestApplication, isDevelopment: boolean) => {
|
||||
scheme: 'Bearer',
|
||||
in: 'header',
|
||||
})
|
||||
.addCookieAuth(IMMICH_ACCESS_COOKIE)
|
||||
.addCookieAuth(ImmichCookie.ACCESS_TOKEN)
|
||||
.addApiKey(
|
||||
{
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: IMMICH_API_KEY_HEADER,
|
||||
name: ImmichHeader.API_KEY,
|
||||
},
|
||||
IMMICH_API_KEY_NAME,
|
||||
Metadata.API_KEY_SECURITY,
|
||||
)
|
||||
.addServer('/api')
|
||||
.build();
|
||||
|
36
server/src/utils/response.ts
Normal file
36
server/src/utils/response.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { CookieOptions, Response } from 'express';
|
||||
import { Duration } from 'luxon';
|
||||
import { CookieResponse, ImmichCookie } from 'src/dtos/auth.dto';
|
||||
|
||||
export const respondWithCookie = <T>(res: Response, body: T, { isSecure, values }: CookieResponse) => {
|
||||
const defaults: CookieOptions = {
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
httpOnly: true,
|
||||
secure: isSecure,
|
||||
maxAge: Duration.fromObject({ days: 400 }).toMillis(),
|
||||
};
|
||||
|
||||
const cookieOptions: Record<ImmichCookie, CookieOptions> = {
|
||||
[ImmichCookie.AUTH_TYPE]: defaults,
|
||||
[ImmichCookie.ACCESS_TOKEN]: defaults,
|
||||
// no httpOnly so that the client can know the auth state
|
||||
[ImmichCookie.IS_AUTHENTICATED]: { ...defaults, httpOnly: false },
|
||||
[ImmichCookie.SHARED_LINK_TOKEN]: { ...defaults, maxAge: Duration.fromObject({ days: 1 }).toMillis() },
|
||||
};
|
||||
|
||||
for (const { key, value } of values) {
|
||||
const options = cookieOptions[key];
|
||||
res.cookie(key, value, options);
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
export const respondWithoutCookie = <T>(res: Response, body: T, cookies: ImmichCookie[]) => {
|
||||
for (const cookie of cookies) {
|
||||
res.clearCookie(cookie);
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
58
server/test/fixtures/auth.stub.ts
vendored
58
server/test/fixtures/auth.stub.ts
vendored
@ -129,51 +129,21 @@ export const loginResponseStub = {
|
||||
},
|
||||
},
|
||||
user1oauth: {
|
||||
response: {
|
||||
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||
userId: 'user-id',
|
||||
userEmail: 'immich@test.com',
|
||||
name: 'immich_name',
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
shouldChangePassword: false,
|
||||
},
|
||||
cookie: [
|
||||
'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;',
|
||||
'immich_auth_type=oauth; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;',
|
||||
'immich_is_authenticated=true; Secure; Path=/; Max-Age=34560000; SameSite=Lax;',
|
||||
],
|
||||
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||
userId: 'user-id',
|
||||
userEmail: 'immich@test.com',
|
||||
name: 'immich_name',
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
shouldChangePassword: false,
|
||||
},
|
||||
user1password: {
|
||||
response: {
|
||||
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||
userId: 'user-id',
|
||||
userEmail: 'immich@test.com',
|
||||
name: 'immich_name',
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
shouldChangePassword: false,
|
||||
},
|
||||
cookie: [
|
||||
'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;',
|
||||
'immich_auth_type=password; HttpOnly; Secure; Path=/; Max-Age=34560000; SameSite=Lax;',
|
||||
'immich_is_authenticated=true; Secure; Path=/; Max-Age=34560000; SameSite=Lax;',
|
||||
],
|
||||
},
|
||||
user1insecure: {
|
||||
response: {
|
||||
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||
userId: 'user-id',
|
||||
userEmail: 'immich@test.com',
|
||||
name: 'immich_name',
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
shouldChangePassword: false,
|
||||
},
|
||||
cookie: [
|
||||
'immich_access_token=cmFuZG9tLWJ5dGVz; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;',
|
||||
'immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;',
|
||||
'immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;',
|
||||
],
|
||||
accessToken: 'cmFuZG9tLWJ5dGVz',
|
||||
userId: 'user-id',
|
||||
userEmail: 'immich@test.com',
|
||||
name: 'immich_name',
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
shouldChangePassword: false,
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user