mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05: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
					
				@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user