Browse Source

Added Cookie Authentication (#360)

* Added Cookie Authentication

* Fixed issue with bearer is in lower case

* Fixed bearer to Bearer to conform with standard
Alex 3 years ago
parent
commit
be3e3e5d7e

+ 1 - 1
mobile/lib/shared/services/api.service.dart

@@ -25,6 +25,6 @@ class ApiService {
   }
   }
 
 
   setAccessToken(String accessToken) {
   setAccessToken(String accessToken) {
-    _apiClient.addDefaultHeader('Authorization', 'bearer $accessToken');
+    _apiClient.addDefaultHeader('Authorization', 'Bearer $accessToken');
   }
   }
 }
 }

+ 16 - 4
server/apps/immich/src/api-v1/auth/auth.controller.ts

@@ -1,4 +1,4 @@
-import { Body, Controller, Post, UseGuards, ValidationPipe } from '@nestjs/common';
+import { Body, Controller, Post, Res, UseGuards, ValidationPipe } from '@nestjs/common';
 import {
 import {
   ApiBadRequestResponse,
   ApiBadRequestResponse,
   ApiBearerAuth,
   ApiBearerAuth,
@@ -15,15 +15,27 @@ import { LoginResponseDto } from './response-dto/login-response.dto';
 import { SignUpDto } from './dto/sign-up.dto';
 import { SignUpDto } from './dto/sign-up.dto';
 import { AdminSignupResponseDto } from './response-dto/admin-signup-response.dto';
 import { AdminSignupResponseDto } from './response-dto/admin-signup-response.dto';
 import { ValidateAccessTokenResponseDto } from './response-dto/validate-asset-token-response.dto,';
 import { ValidateAccessTokenResponseDto } from './response-dto/validate-asset-token-response.dto,';
-
+import { Response } from 'express';
 @ApiTags('Authentication')
 @ApiTags('Authentication')
 @Controller('auth')
 @Controller('auth')
 export class AuthController {
 export class AuthController {
   constructor(private readonly authService: AuthService) {}
   constructor(private readonly authService: AuthService) {}
 
 
   @Post('/login')
   @Post('/login')
-  async login(@Body(ValidationPipe) loginCredential: LoginCredentialDto): Promise<LoginResponseDto> {
-    return await this.authService.login(loginCredential);
+  async login(
+    @Body(ValidationPipe) loginCredential: LoginCredentialDto,
+    @Res() response: Response,
+  ): Promise<LoginResponseDto> {
+    const loginResponse = await this.authService.login(loginCredential);
+
+    // Set Cookies
+    const accessTokenCookie = this.authService.getCookieWithJwtToken(loginResponse);
+    const isAuthCookie = `immich_is_authenticated=true; Path=/; Max-Age=${7 * 24 * 3600}`;
+
+    response.setHeader('Set-Cookie', [accessTokenCookie, isAuthCookie]);
+    response.send(loginResponse);
+
+    return loginResponse;
   }
   }
 
 
   @Post('/admin-sign-up')
   @Post('/admin-sign-up')

+ 6 - 0
server/apps/immich/src/api-v1/auth/auth.service.ts

@@ -63,6 +63,12 @@ export class AuthService {
     return mapLoginResponse(validatedUser, accessToken);
     return mapLoginResponse(validatedUser, accessToken);
   }
   }
 
 
+  public getCookieWithJwtToken(authLoginInfo: LoginResponseDto) {
+    const maxAge = 7 * 24 * 3600; // 7 days
+    return `immich_access_token=${authLoginInfo.accessToken}; HttpOnly; Path=/; Max-Age=${maxAge}`;
+  }
+
+  // !TODO: refactor this method to use the userService createUser method
   public async adminSignUp(signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
   public async adminSignUp(signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
     const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
     const adminUser = await this.userRepository.findOne({ where: { isAdmin: true } });
 
 

+ 1 - 1
server/apps/immich/src/config/jwt.config.ts

@@ -3,5 +3,5 @@ import { jwtSecret } from '../constants/jwt.constant';
 
 
 export const jwtConfig: JwtModuleOptions = {
 export const jwtConfig: JwtModuleOptions = {
   secret: jwtSecret,
   secret: jwtSecret,
-  signOptions: { expiresIn: '36500d' },
+  signOptions: { expiresIn: '7d' },
 };
 };

+ 3 - 2
server/apps/immich/src/main.ts

@@ -3,6 +3,7 @@ import { Logger } from '@nestjs/common';
 import { NestFactory } from '@nestjs/core';
 import { NestFactory } from '@nestjs/core';
 import { NestExpressApplication } from '@nestjs/platform-express';
 import { NestExpressApplication } from '@nestjs/platform-express';
 import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger';
 import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger';
+import cookieParser from 'cookie-parser';
 import { writeFileSync } from 'fs';
 import { writeFileSync } from 'fs';
 import path from 'path';
 import path from 'path';
 import { AppModule } from './app.module';
 import { AppModule } from './app.module';
@@ -12,7 +13,7 @@ async function bootstrap() {
   const app = await NestFactory.create<NestExpressApplication>(AppModule);
   const app = await NestFactory.create<NestExpressApplication>(AppModule);
 
 
   app.set('trust proxy');
   app.set('trust proxy');
-
+  app.use(cookieParser());
   if (process.env.NODE_ENV === 'development') {
   if (process.env.NODE_ENV === 'development') {
     app.enableCors();
     app.enableCors();
   }
   }
@@ -25,7 +26,7 @@ async function bootstrap() {
     .setVersion('1.17.0')
     .setVersion('1.17.0')
     .addBearerAuth({
     .addBearerAuth({
       type: 'http',
       type: 'http',
-      scheme: 'bearer',
+      scheme: 'Bearer',
       bearerFormat: 'JWT',
       bearerFormat: 'JWT',
       name: 'JWT',
       name: 'JWT',
       description: 'Enter JWT token',
       description: 'Enter JWT token',

+ 21 - 0
server/apps/immich/src/modules/immich-jwt/immich-jwt.service.ts

@@ -1,5 +1,6 @@
 import { Injectable, Logger } from '@nestjs/common';
 import { Injectable, Logger } from '@nestjs/common';
 import { JwtService } from '@nestjs/jwt';
 import { JwtService } from '@nestjs/jwt';
+import { Request } from 'express';
 import { JwtPayloadDto } from '../../api-v1/auth/dto/jwt-payload.dto';
 import { JwtPayloadDto } from '../../api-v1/auth/dto/jwt-payload.dto';
 import { jwtSecret } from '../../constants/jwt.constant';
 import { jwtSecret } from '../../constants/jwt.constant';
 
 
@@ -33,4 +34,24 @@ export class ImmichJwtService {
       };
       };
     }
     }
   }
   }
+
+  public extractJwtFromHeader(req: Request) {
+    if (
+      req.headers.authorization &&
+      (req.headers.authorization.split(' ')[0] === 'Bearer' || req.headers.authorization.split(' ')[0] === 'bearer')
+    ) {
+      const accessToken = req.headers.authorization.split(' ')[1];
+      return accessToken;
+    }
+
+    return null;
+  }
+
+  public extractJwtFromCookie(req: Request) {
+    if (req.cookies?.immich_access_token) {
+      return req.cookies.immich_access_token;
+    }
+
+    return null;
+  }
 }
 }

+ 7 - 1
server/apps/immich/src/modules/immich-jwt/strategies/jwt.strategy.ts

@@ -6,15 +6,21 @@ import { Repository } from 'typeorm';
 import { JwtPayloadDto } from '../../../api-v1/auth/dto/jwt-payload.dto';
 import { JwtPayloadDto } from '../../../api-v1/auth/dto/jwt-payload.dto';
 import { UserEntity } from '@app/database/entities/user.entity';
 import { UserEntity } from '@app/database/entities/user.entity';
 import { jwtSecret } from '../../../constants/jwt.constant';
 import { jwtSecret } from '../../../constants/jwt.constant';
+import { ImmichJwtService } from '../immich-jwt.service';
 
 
 @Injectable()
 @Injectable()
 export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
 export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
   constructor(
   constructor(
     @InjectRepository(UserEntity)
     @InjectRepository(UserEntity)
     private usersRepository: Repository<UserEntity>,
     private usersRepository: Repository<UserEntity>,
+
+    private immichJwtService: ImmichJwtService,
   ) {
   ) {
     super({
     super({
-      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
+      jwtFromRequest: ExtractJwt.fromExtractors([
+        immichJwtService.extractJwtFromHeader,
+        immichJwtService.extractJwtFromCookie,
+      ]),
       ignoreExpiration: false,
       ignoreExpiration: false,
       secretOrKey: jwtSecret,
       secretOrKey: jwtSecret,
     });
     });

File diff suppressed because it is too large
+ 0 - 0
server/immich-openapi-specs.json


+ 56 - 0
server/package-lock.json

@@ -30,6 +30,7 @@
         "bull": "^4.4.0",
         "bull": "^4.4.0",
         "class-transformer": "^0.5.1",
         "class-transformer": "^0.5.1",
         "class-validator": "^0.13.2",
         "class-validator": "^0.13.2",
+        "cookie-parser": "^1.4.6",
         "diskusage": "^1.1.3",
         "diskusage": "^1.1.3",
         "dotenv": "^14.2.0",
         "dotenv": "^14.2.0",
         "exifr": "^7.1.3",
         "exifr": "^7.1.3",
@@ -56,6 +57,7 @@
         "@openapitools/openapi-generator-cli": "2.5.1",
         "@openapitools/openapi-generator-cli": "2.5.1",
         "@types/bcrypt": "^5.0.0",
         "@types/bcrypt": "^5.0.0",
         "@types/bull": "^3.15.7",
         "@types/bull": "^3.15.7",
+        "@types/cookie-parser": "^1.4.3",
         "@types/cron": "^2.0.0",
         "@types/cron": "^2.0.0",
         "@types/express": "^4.17.13",
         "@types/express": "^4.17.13",
         "@types/fluent-ffmpeg": "^2.1.20",
         "@types/fluent-ffmpeg": "^2.1.20",
@@ -2412,6 +2414,15 @@
       "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
       "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
       "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
       "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
     },
     },
+    "node_modules/@types/cookie-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
+      "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
+      "dev": true,
+      "dependencies": {
+        "@types/express": "*"
+      }
+    },
     "node_modules/@types/cookiejar": {
     "node_modules/@types/cookiejar": {
       "version": "2.1.2",
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
       "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
@@ -4401,6 +4412,26 @@
         "node": ">= 0.6"
         "node": ">= 0.6"
       }
       }
     },
     },
+    "node_modules/cookie-parser": {
+      "version": "1.4.6",
+      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
+      "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+      "dependencies": {
+        "cookie": "0.4.1",
+        "cookie-signature": "1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/cookie-parser/node_modules/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/cookie-signature": {
     "node_modules/cookie-signature": {
       "version": "1.0.6",
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -13280,6 +13311,15 @@
       "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
       "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz",
       "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
       "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
     },
     },
+    "@types/cookie-parser": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
+      "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
+      "dev": true,
+      "requires": {
+        "@types/express": "*"
+      }
+    },
     "@types/cookiejar": {
     "@types/cookiejar": {
       "version": "2.1.2",
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
       "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz",
@@ -14908,6 +14948,22 @@
       "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
       "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
       "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
       "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw=="
     },
     },
+    "cookie-parser": {
+      "version": "1.4.6",
+      "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
+      "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+      "requires": {
+        "cookie": "0.4.1",
+        "cookie-signature": "1.0.6"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+          "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
+        }
+      }
+    },
     "cookie-signature": {
     "cookie-signature": {
       "version": "1.0.6",
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",

+ 3 - 1
server/package.json

@@ -49,6 +49,7 @@
     "bull": "^4.4.0",
     "bull": "^4.4.0",
     "class-transformer": "^0.5.1",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.13.2",
     "class-validator": "^0.13.2",
+    "cookie-parser": "^1.4.6",
     "diskusage": "^1.1.3",
     "diskusage": "^1.1.3",
     "dotenv": "^14.2.0",
     "dotenv": "^14.2.0",
     "exifr": "^7.1.3",
     "exifr": "^7.1.3",
@@ -72,8 +73,10 @@
     "@nestjs/cli": "^8.2.8",
     "@nestjs/cli": "^8.2.8",
     "@nestjs/schematics": "^8.0.11",
     "@nestjs/schematics": "^8.0.11",
     "@nestjs/testing": "^8.4.7",
     "@nestjs/testing": "^8.4.7",
+    "@openapitools/openapi-generator-cli": "2.5.1",
     "@types/bcrypt": "^5.0.0",
     "@types/bcrypt": "^5.0.0",
     "@types/bull": "^3.15.7",
     "@types/bull": "^3.15.7",
+    "@types/cookie-parser": "^1.4.3",
     "@types/cron": "^2.0.0",
     "@types/cron": "^2.0.0",
     "@types/express": "^4.17.13",
     "@types/express": "^4.17.13",
     "@types/fluent-ffmpeg": "^2.1.20",
     "@types/fluent-ffmpeg": "^2.1.20",
@@ -88,7 +91,6 @@
     "@types/supertest": "^2.0.11",
     "@types/supertest": "^2.0.11",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/eslint-plugin": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
     "@typescript-eslint/parser": "^5.0.0",
-    "@openapitools/openapi-generator-cli": "2.5.1",
     "eslint": "^8.0.1",
     "eslint": "^8.0.1",
     "eslint-config-prettier": "^8.3.0",
     "eslint-config-prettier": "^8.3.0",
     "eslint-plugin-prettier": "^4.0.0",
     "eslint-plugin-prettier": "^4.0.0",

+ 1 - 1
web/src/api/api.ts

@@ -6,7 +6,7 @@ import {
 	Configuration,
 	Configuration,
 	DeviceInfoApi,
 	DeviceInfoApi,
 	ServerInfoApi,
 	ServerInfoApi,
-	UserApi,
+	UserApi
 } from './open-api';
 } from './open-api';
 
 
 class ImmichApi {
 class ImmichApi {

Some files were not shown because too many files changed in this diff