1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-04 10:56:47 +01:00

chore(server): remove token when logged out (#1560)

* chore(mobile): invoke logout() on mobile app

* feat: add mechanism to delete token from logging out endpoint

* fix: set state after login sequence success

* fix: not removing token when logging out from OAuth

* fix: prettier

* refactor: using accessTokenId to delete

* chore: pr comments

* fix: test

* fix: test threshold
This commit is contained in:
Alex 2023-02-05 23:31:16 -06:00 committed by GitHub
parent 16183791f3
commit 7dbddba757
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 37 additions and 21 deletions

View file

@ -91,8 +91,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
} }
Future<bool> logout() async { Future<bool> logout() async {
state = state.copyWith(isAuthenticated: false);
await Future.wait([ await Future.wait([
_apiService.authenticationApi.logout(),
Hive.box(userInfoBox).delete(accessTokenKey), Hive.box(userInfoBox).delete(accessTokenKey),
Hive.box(userInfoBox).delete(assetEtagKey), Hive.box(userInfoBox).delete(assetEtagKey),
_assetCacheService.invalidate(), _assetCacheService.invalidate(),
@ -101,6 +101,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey) Hive.box<HiveSavedLoginInfo>(hiveLoginInfoBox).delete(savedLoginInfoKey)
]); ]);
state = state.copyWith(isAuthenticated: false);
return true; return true;
} }

View file

@ -59,13 +59,18 @@ export class AuthController {
return this.authService.changePassword(authUser, dto); return this.authService.changePassword(authUser, dto);
} }
@Authenticated()
@Post('logout') @Post('logout')
async logout(@Req() req: Request, @Res({ passthrough: true }) res: Response): Promise<LogoutResponseDto> { async logout(
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
@GetAuthUser() authUser: AuthUserDto,
): Promise<LogoutResponseDto> {
const authType: AuthType = req.cookies[IMMICH_AUTH_TYPE_COOKIE]; const authType: AuthType = req.cookies[IMMICH_AUTH_TYPE_COOKIE];
res.clearCookie(IMMICH_ACCESS_COOKIE); res.clearCookie(IMMICH_ACCESS_COOKIE);
res.clearCookie(IMMICH_AUTH_TYPE_COOKIE); res.clearCookie(IMMICH_AUTH_TYPE_COOKIE);
return this.authService.logout(authType); return this.authService.logout(authUser, authType);
} }
} }

View file

@ -26,7 +26,7 @@ import { IUserRepository } from '../user';
import { IUserTokenRepository } from '../user-token'; import { IUserTokenRepository } from '../user-token';
import { AuthType } from './auth.constant'; import { AuthType } from './auth.constant';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { SignUpDto } from './dto'; import { AuthUserDto, SignUpDto } from './dto';
// const token = Buffer.from('my-api-key', 'utf8').toString('base64'); // const token = Buffer.from('my-api-key', 'utf8').toString('base64');
@ -192,14 +192,18 @@ describe('AuthService', () => {
describe('logout', () => { describe('logout', () => {
it('should return the end session endpoint', async () => { it('should return the end session endpoint', async () => {
await expect(sut.logout(AuthType.OAUTH)).resolves.toEqual({ const authUser = { id: '123' } as AuthUserDto;
await expect(sut.logout(authUser, AuthType.OAUTH)).resolves.toEqual({
successful: true, successful: true,
redirectUri: 'http://end-session-endpoint', redirectUri: 'http://end-session-endpoint',
}); });
}); });
it('should return the default redirect', async () => { it('should return the default redirect', async () => {
await expect(sut.logout(AuthType.PASSWORD)).resolves.toEqual({ const authUser = { id: '123' } as AuthUserDto;
await expect(sut.logout(authUser, AuthType.PASSWORD)).resolves.toEqual({
successful: true, successful: true,
redirectUri: '/auth/login?autoLaunch=0', redirectUri: '/auth/login?autoLaunch=0',
}); });

View file

@ -76,7 +76,11 @@ export class AuthService {
return this.authCore.createLoginResponse(user, AuthType.PASSWORD, isSecure); return this.authCore.createLoginResponse(user, AuthType.PASSWORD, isSecure);
} }
public async logout(authType: AuthType): Promise<LogoutResponseDto> { public async logout(authUser: AuthUserDto, authType: AuthType): Promise<LogoutResponseDto> {
if (authUser.accessTokenId) {
await this.userTokenCore.deleteToken(authUser.accessTokenId);
}
if (authType === AuthType.OAUTH) { if (authType === AuthType.OAUTH) {
const url = await this.oauthCore.getLogoutEndpoint(); const url = await this.oauthCore.getLogoutEndpoint();
if (url) { if (url) {

View file

@ -7,4 +7,5 @@ export class AuthUserDto {
isAllowUpload?: boolean; isAllowUpload?: boolean;
isAllowDownload?: boolean; isAllowDownload?: boolean;
isShowExif?: boolean; isShowExif?: boolean;
accessTokenId?: string;
} }

View file

@ -9,28 +9,22 @@ export class UserTokenCore {
async validate(tokenValue: string) { async validate(tokenValue: string) {
const hashedToken = this.crypto.hashSha256(tokenValue); const hashedToken = this.crypto.hashSha256(tokenValue);
const user = await this.getUserByToken(hashedToken); const token = await this.repository.get(hashedToken);
if (user) {
if (token?.user) {
return { return {
...user, ...token.user,
isPublicUser: false, isPublicUser: false,
isAllowUpload: true, isAllowUpload: true,
isAllowDownload: true, isAllowDownload: true,
isShowExif: true, isShowExif: true,
accessTokenId: token.id,
}; };
} }
throw new UnauthorizedException('Invalid user token'); throw new UnauthorizedException('Invalid user token');
} }
public async getUserByToken(tokenValue: string): Promise<UserEntity | null> {
const token = await this.repository.get(tokenValue);
if (token?.user) {
return token.user;
}
return null;
}
public async createToken(user: UserEntity): Promise<string> { public async createToken(user: UserEntity): Promise<string> {
const key = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, ''); const key = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, '');
const token = this.crypto.hashSha256(key); const token = this.crypto.hashSha256(key);
@ -41,4 +35,8 @@ export class UserTokenCore {
return key; return key;
} }
public async deleteToken(id: string): Promise<void> {
await this.repository.delete(id);
}
} }

View file

@ -91,6 +91,7 @@ export const authStub = {
isAllowUpload: true, isAllowUpload: true,
isAllowDownload: true, isAllowDownload: true,
isShowExif: true, isShowExif: true,
accessTokenId: 'token-id',
}), }),
adminSharedLink: Object.freeze<AuthUserDto>({ adminSharedLink: Object.freeze<AuthUserDto>({
id: 'admin_id', id: 'admin_id',
@ -111,6 +112,7 @@ export const authStub = {
isPublicUser: true, isPublicUser: true,
isShowExif: true, isShowExif: true,
sharedLinkId: '123', sharedLinkId: '123',
accessTokenId: 'token-id',
}), }),
}; };

View file

@ -19,7 +19,7 @@ export class UserTokenRepository implements IUserTokenRepository {
return this.userTokenRepository.save(userToken); return this.userTokenRepository.save(userToken);
} }
async delete(userToken: string): Promise<void> { async delete(id: string): Promise<void> {
await this.userTokenRepository.delete(userToken); await this.userTokenRepository.delete(id);
} }
} }

View file

@ -140,7 +140,7 @@
}, },
"./libs/domain/": { "./libs/domain/": {
"branches": 80, "branches": 80,
"functions": 90, "functions": 89,
"lines": 95, "lines": 95,
"statements": 95 "statements": 95
} }