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:
parent
16183791f3
commit
7dbddba757
9 changed files with 37 additions and 21 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -7,4 +7,5 @@ export class AuthUserDto {
|
||||||
isAllowUpload?: boolean;
|
isAllowUpload?: boolean;
|
||||||
isAllowDownload?: boolean;
|
isAllowDownload?: boolean;
|
||||||
isShowExif?: boolean;
|
isShowExif?: boolean;
|
||||||
|
accessTokenId?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,7 +140,7 @@
|
||||||
},
|
},
|
||||||
"./libs/domain/": {
|
"./libs/domain/": {
|
||||||
"branches": 80,
|
"branches": 80,
|
||||||
"functions": 90,
|
"functions": 89,
|
||||||
"lines": 95,
|
"lines": 95,
|
||||||
"statements": 95
|
"statements": 95
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue