mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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:
		@@ -59,13 +59,18 @@ export class AuthController {
 | 
			
		||||
    return this.authService.changePassword(authUser, dto);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Authenticated()
 | 
			
		||||
  @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];
 | 
			
		||||
 | 
			
		||||
    res.clearCookie(IMMICH_ACCESS_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 { AuthType } from './auth.constant';
 | 
			
		||||
import { AuthService } from './auth.service';
 | 
			
		||||
import { SignUpDto } from './dto';
 | 
			
		||||
import { AuthUserDto, SignUpDto } from './dto';
 | 
			
		||||
 | 
			
		||||
// const token = Buffer.from('my-api-key', 'utf8').toString('base64');
 | 
			
		||||
 | 
			
		||||
@@ -192,14 +192,18 @@ describe('AuthService', () => {
 | 
			
		||||
 | 
			
		||||
  describe('logout', () => {
 | 
			
		||||
    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,
 | 
			
		||||
        redirectUri: 'http://end-session-endpoint',
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
        redirectUri: '/auth/login?autoLaunch=0',
 | 
			
		||||
      });
 | 
			
		||||
 
 | 
			
		||||
@@ -76,7 +76,11 @@ export class AuthService {
 | 
			
		||||
    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) {
 | 
			
		||||
      const url = await this.oauthCore.getLogoutEndpoint();
 | 
			
		||||
      if (url) {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,4 +7,5 @@ export class AuthUserDto {
 | 
			
		||||
  isAllowUpload?: boolean;
 | 
			
		||||
  isAllowDownload?: boolean;
 | 
			
		||||
  isShowExif?: boolean;
 | 
			
		||||
  accessTokenId?: string;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,28 +9,22 @@ export class UserTokenCore {
 | 
			
		||||
 | 
			
		||||
  async validate(tokenValue: string) {
 | 
			
		||||
    const hashedToken = this.crypto.hashSha256(tokenValue);
 | 
			
		||||
    const user = await this.getUserByToken(hashedToken);
 | 
			
		||||
    if (user) {
 | 
			
		||||
    const token = await this.repository.get(hashedToken);
 | 
			
		||||
 | 
			
		||||
    if (token?.user) {
 | 
			
		||||
      return {
 | 
			
		||||
        ...user,
 | 
			
		||||
        ...token.user,
 | 
			
		||||
        isPublicUser: false,
 | 
			
		||||
        isAllowUpload: true,
 | 
			
		||||
        isAllowDownload: true,
 | 
			
		||||
        isShowExif: true,
 | 
			
		||||
        accessTokenId: token.id,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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> {
 | 
			
		||||
    const key = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, '');
 | 
			
		||||
    const token = this.crypto.hashSha256(key);
 | 
			
		||||
@@ -41,4 +35,8 @@ export class UserTokenCore {
 | 
			
		||||
 | 
			
		||||
    return key;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async deleteToken(id: string): Promise<void> {
 | 
			
		||||
    await this.repository.delete(id);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -91,6 +91,7 @@ export const authStub = {
 | 
			
		||||
    isAllowUpload: true,
 | 
			
		||||
    isAllowDownload: true,
 | 
			
		||||
    isShowExif: true,
 | 
			
		||||
    accessTokenId: 'token-id',
 | 
			
		||||
  }),
 | 
			
		||||
  adminSharedLink: Object.freeze<AuthUserDto>({
 | 
			
		||||
    id: 'admin_id',
 | 
			
		||||
@@ -111,6 +112,7 @@ export const authStub = {
 | 
			
		||||
    isPublicUser: true,
 | 
			
		||||
    isShowExif: true,
 | 
			
		||||
    sharedLinkId: '123',
 | 
			
		||||
    accessTokenId: 'token-id',
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,7 @@ export class UserTokenRepository implements IUserTokenRepository {
 | 
			
		||||
    return this.userTokenRepository.save(userToken);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async delete(userToken: string): Promise<void> {
 | 
			
		||||
    await this.userTokenRepository.delete(userToken);
 | 
			
		||||
  async delete(id: string): Promise<void> {
 | 
			
		||||
    await this.userTokenRepository.delete(id);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -140,7 +140,7 @@
 | 
			
		||||
      },
 | 
			
		||||
      "./libs/domain/": {
 | 
			
		||||
        "branches": 80,
 | 
			
		||||
        "functions": 90,
 | 
			
		||||
        "functions": 89,
 | 
			
		||||
        "lines": 95,
 | 
			
		||||
        "statements": 95
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user