From 754d4560eab0bf81d92914f85b99d9a64162a5a5 Mon Sep 17 00:00:00 2001 From: Pujit Mehrotra Date: Fri, 11 Oct 2024 10:45:03 -0400 Subject: [PATCH] feat: integrate cross-domain authentication to api --- api/dev/Unraid.net/myservers.cfg | 4 ++-- api/dev/states/myservers.cfg | 8 ++++---- api/package-lock.json | 20 ++++++++++++++++++++ api/package.json | 1 + api/src/unraid-api/app/cors.ts | 6 +++--- api/src/unraid-api/auth/auth.guard.ts | 3 ++- api/src/unraid-api/auth/auth.module.ts | 2 ++ api/src/unraid-api/auth/auth.service.ts | 11 +++++++++-- api/src/unraid-api/auth/cookie.strategy.ts | 22 ++++++++++++++++++++++ api/src/unraid-api/users/users.service.ts | 15 +++++++++++++++ web/components/Notifications/Sidebar.vue | 9 +++++---- web/store/unraidApi.ts | 4 +++- 12 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 api/src/unraid-api/auth/cookie.strategy.ts diff --git a/api/dev/Unraid.net/myservers.cfg b/api/dev/Unraid.net/myservers.cfg index 2d129be6e..518369adf 100644 --- a/api/dev/Unraid.net/myservers.cfg +++ b/api/dev/Unraid.net/myservers.cfg @@ -1,6 +1,6 @@ [api] -version="3.11.0+3f537b97" -extraOrigins="https://google.com,https://test.com,http://localhost:4321" +version="3.11.0+04f7b636" +extraOrigins="https://google.com,https://test.com" [local] [notifier] apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5" diff --git a/api/dev/states/myservers.cfg b/api/dev/states/myservers.cfg index 3faf6e250..328165ca8 100644 --- a/api/dev/states/myservers.cfg +++ b/api/dev/states/myservers.cfg @@ -1,6 +1,6 @@ [api] -version="3.11.0+3f537b97" -extraOrigins="https://google.com,https://test.com,http://localhost:4321" +version="3.11.0+04f7b636" +extraOrigins="https://google.com,https://test.com" [local] [notifier] apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5" @@ -16,9 +16,9 @@ regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0" idtoken="" accesstoken="" refreshtoken="" -allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, http://localhost:4321, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com" +allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com" dynamicRemoteAccessType="DISABLED" [upc] apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810" [connectionStatus] -minigraph="CONNECTED" +minigraph="ERROR_RETRYING" diff --git a/api/package-lock.json b/api/package-lock.json index 2f9d98c67..c9dd5b8fd 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -75,6 +75,7 @@ "openid-client": "^5.6.5", "p-iteration": "^1.1.8", "p-retry": "^4.6.2", + "passport-custom": "^1.1.1", "passport-http-header-strategy": "^1.1.0", "pidusage": "^3.0.2", "pino": "^9.1.0", @@ -14492,6 +14493,17 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-custom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", + "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/passport-http-header-strategy": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz", @@ -29188,6 +29200,14 @@ "utils-merge": "^1.0.1" } }, + "passport-custom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", + "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", + "requires": { + "passport-strategy": "1.x.x" + } + }, "passport-http-header-strategy": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz", diff --git a/api/package.json b/api/package.json index 7aa87ff07..e0e0d77a2 100644 --- a/api/package.json +++ b/api/package.json @@ -126,6 +126,7 @@ "openid-client": "^5.6.5", "p-iteration": "^1.1.8", "p-retry": "^4.6.2", + "passport-custom": "^1.1.1", "passport-http-header-strategy": "^1.1.0", "pidusage": "^3.0.2", "pino": "^9.1.0", diff --git a/api/src/unraid-api/app/cors.ts b/api/src/unraid-api/app/cors.ts index 340c401e7..4cc86dc15 100644 --- a/api/src/unraid-api/app/cors.ts +++ b/api/src/unraid-api/app/cors.ts @@ -69,12 +69,12 @@ export const configureFastifyCors = if (typeof cookies === 'object') { service.hasValidAuthCookie(cookies).then((isValid) => { if (isValid) { - callback(null, { origin: true }); + callback(null, { credentials: true, origin: true }); } else { - callback(null, { origin: isOriginAllowed }); + callback(null, { credentials: true, origin: isOriginAllowed }); } }); } else { - callback(null, { origin: isOriginAllowed }); + callback(null, { credentials: true, origin: isOriginAllowed }); } }; diff --git a/api/src/unraid-api/auth/auth.guard.ts b/api/src/unraid-api/auth/auth.guard.ts index c9c2658a3..a97fd273d 100644 --- a/api/src/unraid-api/auth/auth.guard.ts +++ b/api/src/unraid-api/auth/auth.guard.ts @@ -11,10 +11,11 @@ import { Reflector } from '@nestjs/core'; import { GqlExecutionContext, type GqlContextType } from '@nestjs/graphql'; import { AuthGuard } from '@nestjs/passport'; import { type Observable } from 'rxjs'; +import { UserCookieStrategy } from './cookie.strategy'; @Injectable() export class GraphqlAuthGuard - extends AuthGuard([ServerHeaderStrategy.key]) + extends AuthGuard([ServerHeaderStrategy.key, UserCookieStrategy.key]) implements CanActivate { constructor(private readonly reflector: Reflector) { diff --git a/api/src/unraid-api/auth/auth.module.ts b/api/src/unraid-api/auth/auth.module.ts index bd81d2184..0837f8381 100644 --- a/api/src/unraid-api/auth/auth.module.ts +++ b/api/src/unraid-api/auth/auth.module.ts @@ -4,12 +4,14 @@ import { UsersModule } from '@app/unraid-api/users/users.module'; import { PassportModule } from '@nestjs/passport'; import { ServerHeaderStrategy } from '@app/unraid-api/auth/header.strategy'; import { CookieService, SESSION_COOKIE_CONFIG } from './cookie.service'; +import { UserCookieStrategy } from './cookie.strategy'; @Module({ imports: [UsersModule, PassportModule], providers: [ AuthService, ServerHeaderStrategy, + UserCookieStrategy, CookieService, { provide: SESSION_COOKIE_CONFIG, useValue: CookieService.defaultOpts() }, ], diff --git a/api/src/unraid-api/auth/auth.service.ts b/api/src/unraid-api/auth/auth.service.ts index 72a3a4622..e910e7c16 100644 --- a/api/src/unraid-api/auth/auth.service.ts +++ b/api/src/unraid-api/auth/auth.service.ts @@ -1,17 +1,24 @@ import { type UserAccount } from '@app/graphql/generated/api/types'; import { UsersService } from '@app/unraid-api/users/users.service'; import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { CookieService } from './cookie.service'; @Injectable() export class AuthService { - constructor(private usersService: UsersService) {} + constructor(private usersService: UsersService, private cookieService: CookieService) {} async validateUser(apiKey: string): Promise { - const user = this.usersService.findOne(apiKey); if (user) { return user; } throw new UnauthorizedException('Invalid API key'); } + + async validateCookies(cookies: object): Promise { + if (await this.cookieService.hasValidAuthCookie(cookies)) { + return this.usersService.getSessionUser(); + } + throw new UnauthorizedException('No user session found'); + } } diff --git a/api/src/unraid-api/auth/cookie.strategy.ts b/api/src/unraid-api/auth/cookie.strategy.ts new file mode 100644 index 000000000..db7c25787 --- /dev/null +++ b/api/src/unraid-api/auth/cookie.strategy.ts @@ -0,0 +1,22 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-custom'; +import { Injectable, Logger } from '@nestjs/common'; +import { AuthService } from './auth.service'; + +const strategyName = 'user-cookie'; + +@Injectable() +export class UserCookieStrategy extends PassportStrategy(Strategy, strategyName) { + static key = strategyName; + private readonly logger = new Logger(UserCookieStrategy.name); + + constructor( + private readonly authService: AuthService, + ) { + super(); + } + + public validate = async (req: any): Promise => { + return this.authService.validateCookies(req.cookies); + }; +} diff --git a/api/src/unraid-api/users/users.service.ts b/api/src/unraid-api/users/users.service.ts index 497d40419..9d51b653e 100644 --- a/api/src/unraid-api/users/users.service.ts +++ b/api/src/unraid-api/users/users.service.ts @@ -46,4 +46,19 @@ export class UsersService { findOne(apiKey: string): UserAccount | null { return this.apiKeyToUser(apiKey); } + + /** + * Returns a user object representing a session. + * Note: Does NOT perform validation. + * + * @returns a service account that represents the user session (i.e. a webgui user). + */ + getSessionUser(): UserAccount { + return { + id: '-1', + description: 'UPC service account', + name: 'upc', + roles: 'upc', + }; + } } diff --git a/web/components/Notifications/Sidebar.vue b/web/components/Notifications/Sidebar.vue index b6a6e8385..21b7c64a4 100644 --- a/web/components/Notifications/Sidebar.vue +++ b/web/components/Notifications/Sidebar.vue @@ -38,11 +38,12 @@ watch(notifications, (newVal) => { const fetchType = ref<"UNREAD" | "ARCHIVED">("UNREAD"); const setFetchType = (type: "UNREAD" | "ARCHIVED") => (fetchType.value = type); -const { unraidApiClient } = storeToRefs(useUnraidApiStore()); +const { unraidApiClient: maybeApi } = storeToRefs(useUnraidApiStore()); -watch(unraidApiClient, async (newVal) => { - if (newVal) { - const apiResponse = await newVal.query({ + +watch(maybeApi, async (apiClient) => { + if (apiClient) { + const apiResponse = await apiClient.query({ query: getNotifications, variables: { filter: { diff --git a/web/store/unraidApi.ts b/web/store/unraidApi.ts index 258db8b60..69542a811 100644 --- a/web/store/unraidApi.ts +++ b/web/store/unraidApi.ts @@ -85,11 +85,13 @@ export const useUnraidApiStore = defineStore('unraidApi', () => { // return; // @todo remove unraidApiStatus.value = 'connecting'; - const headers = { 'x-api-key': serverStore.apiKey }; + // const headers = { 'x-api-key': serverStore.apiKey }; + const headers = {}; const httpLink = createHttpLink({ uri: httpEndpoint.toString(), headers, + credentials: "include", }); const wsLink = new GraphQLWsLink(