Files
omnivore/packages/api/src/routers/auth/apple_auth.ts
Tom Rogers 4e582fb55d Improving Self-Hosting and Removing 3rd Party dependencies. (#4513)
* fix: Library Header layout shift

* Bump Github Actions versions.

* Self-Hosting Changes

* Fix Minio Environment Variable

* Just make pdfs successful, due to lack of PDFHandler

* Fix issue where flag was set wrong

* Added an NGINX Example file

* Add some documentation for self-hosting via Docker Compose

* Make some adjustments to Puppeteer due to failing sites.

* adjust timings

* Add start of Mail Service

* Fix Docker Files

* More email service stuff

* Add Guide to use Zapier for Email-Importing.

* Ensure that if no env is provided it uses the old email settings

* Add some instructions for self-hosted email

* Add SNS Endpoints for Mail Watcher

* Add steps and functionality for using SES and SNS for email

* Uncomment a few jobs.

* Added option for Firefox for parser. Was having issues with Chromium on Docker.

* Add missing space.

Co-authored-by: Russ Taylor <729694+russtaylor@users.noreply.github.com>

* Fix some wording on the Guide

* update browser extension to handle self-hosted instances

* add slight documentation to options page

* Fix MV

* Do raw handlers for Medium

* Fix images in Medium

* Update self-hosting/GUIDE.md

Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com>

* Update Guide with other variables

* Add The Verge to JS-less handlers

* Update regex and image-proxy

* Update self-hosting/nginx/nginx.conf

Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com>

* Update regex and image-proxy

* Update self-hosting/docker-compose/docker-compose.yml

Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com>

* Fix Minio for Export

* Merge to main

* Update GUIDE with newer NGINX

* Update nginx config to include api/save route

* Enable Native PDF View for PDFS

* Enable Native PDF View for PDFS

* feat:lover packages test

* feat:working build

* feat:alpine build

* docs:api dockerfile docs

* Write a PDF.js wrapper to replace pspdfkit

* Revert changes for replication, set settings to have default mode

* build folder got removed due to gitignore on pdf

* Add Box shadow to pdf pages

* Add Toggle for Progress in PDFS, enabled native viewer toggle

* Update node version to LTS

* Update node version to LTS

* Fix Linting issues

* Fix Linting issues

* Make env variable nullable

* Add touchend listener for mobile

* Make changes to PDF for mobile

* fix(android): change serverUrl to selfhosted first

* feat:2 stage alpine content fetch

* feat:separated start script

* fix:changed to node 22

* Add back youtube functionality and add guide

* trigger build

* Fix cache issue on YouTube

* Allow empty AWS_S3_ENDPOINT

* Allow empty AWS_S3_ENDPOINT

* Add GCHR for all images

* Add GCHR For self hosting.

* Add GCHR For self hosting.

* Test prebuilt.

* Test prebuilt

* Test prebuilt...

* Fix web image

* Remove Web Image (For now)

* Move docker-compose to images

* Move docker-compose files to correct locations

* Remove the need for ARGS

* Update packages, and Typescript versions

* Fix

* Fix issues with build on Web

* Correct push

* Fix Linting issues

* Fix Trace import

* Add missing types

* Fix Tasks

* Add information into guide about self-build

* Fix issues with PDF Viewer

---------

Co-authored-by: keumky2 <keumky2@woowahan.com>
Co-authored-by: William Theaker <wtheaker@nvidia.com>
Co-authored-by: Russ Taylor <729694+russtaylor@users.noreply.github.com>
Co-authored-by: David Adams <david@dadams2.com>
Co-authored-by: Mike Baker <1426795+mbaker3@users.noreply.github.com>
Co-authored-by: m1xxos <66390094+m1xxos@users.noreply.github.com>
Co-authored-by: Adil <mr.adil777@gmail.com>
2025-01-27 13:33:16 +01:00

219 lines
5.7 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import * as jwt from 'jsonwebtoken'
import jwksClient from 'jwks-rsa'
import { StatusType } from '../../entity/user'
import { env, homePageURL } from '../../env'
import { LoginErrorCode } from '../../generated/graphql'
import { userRepository } from '../../repository/user'
import { analytics } from '../../utils/analytics'
import { logger } from '../../utils/logger'
import { createSsoToken, ssoRedirectURL } from '../../utils/sso'
import { DecodeTokenResult } from './auth_types'
import {
createPendingUserToken,
createWebAuthToken,
suggestedUsername,
} from './jwt_helpers'
import { DEFAULT_HOME_PATH } from '../../utils/navigation'
const appleBaseURL = 'https://appleid.apple.com'
const audienceName = 'app.omnivore.app'
const webAudienceName = 'app.omnivore'
async function fetchApplePublicKey(kid: string): Promise<string | null> {
const client = jwksClient({
cache: true,
jwksUri: `${appleBaseURL}/auth/keys`,
})
try {
const key: jwksClient.SigningKey = await new Promise((resolve, reject) => {
client.getSigningKey(kid, (error, result) => {
if (error || result === undefined) {
return reject(error)
}
return resolve(result)
})
})
return key.getPublicKey()
} catch (e) {
logger.error('fetchApplePublicKey error', e)
return null
}
}
export async function decodeAppleToken(
token: string
): Promise<DecodeTokenResult> {
const decodedToken = jwt.decode(token, { complete: true })
const { kid, alg } = (decodedToken as any).header
try {
const publicKey = await fetchApplePublicKey(kid)
if (!publicKey) {
return { errorCode: 500 }
}
const jwtClaims: any = jwt.verify(token, publicKey, { algorithms: [alg] })
const issVerified = (jwtClaims.iss ?? '') === appleBaseURL
const audience = jwtClaims.aud ?? ''
const audVerified = audience == webAudienceName || audience === audienceName
if (issVerified && audVerified && jwtClaims.email) {
return {
email: jwtClaims.email,
sourceUserId: jwtClaims.sub,
name: jwtClaims.name,
}
} else {
return {
errorCode: 401,
}
}
} catch (e) {
logger.error('decodeAppleToken error', e)
return { errorCode: 500 }
}
}
type AppleWebAuthResponse = {
redirectURL: string
authToken?: string
pendingUserToken?: string
}
type AppleUserData = {
name?: AppleUserName
email?: string
}
type AppleUserName = {
firstName?: string
lastName?: string
}
export async function handleAppleWebAuth(
idToken: string,
appleUserData?: AppleUserData,
isLocal = false,
isVercel = false
): Promise<AppleWebAuthResponse> {
const baseURL = () => {
if (isLocal) {
return 'http://localhost:3000'
}
if (isVercel) {
return homePageURL()
}
return env.client.url
}
const decodedTokenResult = await decodeAppleToken(idToken)
const authFailedRedirect = `${baseURL()}/login?errorCodes=${
LoginErrorCode.AuthFailed
}`
if (!decodedTokenResult.email || decodedTokenResult.errorCode) {
return Promise.resolve({
redirectURL: authFailedRedirect,
})
}
try {
const user = await userRepository.findOneBy({
sourceUserId: decodedTokenResult.sourceUserId,
source: 'APPLE',
status: StatusType.Active,
})
const userId = user?.id
if (!userId) {
// create a temp token so the user can create a new profile
const payload = await createTempAppleUserPayload({
authFailedRedirect,
appleUserData,
baseURL: baseURL(),
sourceUserId: decodedTokenResult.sourceUserId,
email: decodedTokenResult.email,
})
return payload
}
const authToken = await createWebAuthToken(userId)
if (authToken) {
const ssoToken = createSsoToken(
authToken,
`${baseURL()}${DEFAULT_HOME_PATH}`
)
const redirectURL = isVercel
? ssoRedirectURL(ssoToken)
: `${baseURL()}${DEFAULT_HOME_PATH}`
analytics.capture({
distinctId: user.id,
event: 'login',
properties: {
method: 'apple',
email: user.email,
username: user.profile.username,
env: env.server.apiEnv,
},
})
return {
authToken,
redirectURL,
}
} else {
return { redirectURL: authFailedRedirect }
}
} catch (e) {
logger.info('handleAppleWebAuth error', e)
return { redirectURL: authFailedRedirect }
}
}
type CreateTempAppleUserPayloadInputs = {
appleUserData?: AppleUserData
authFailedRedirect: string
baseURL: string
sourceUserId?: string
email?: string
}
async function createTempAppleUserPayload(
inputs: CreateTempAppleUserPayloadInputs
): Promise<AppleWebAuthResponse> {
if (!inputs.email || !inputs.sourceUserId) {
throw new Error('missing email or sourceUserId')
}
const firstName = inputs.appleUserData?.name?.firstName ?? ''
const lastName = inputs.appleUserData?.name?.lastName ?? ''
const name = `${firstName} ${lastName}`
const username = suggestedUsername(name)
try {
const pendingUserToken = await createPendingUserToken({
email: inputs.email,
sourceUserId: inputs.sourceUserId,
provider: 'APPLE',
name,
username,
})
if (!pendingUserToken) {
throw new Error('Failed to create pending user token')
}
return {
redirectURL: `${inputs.baseURL}/confirm-profile?username=${username}&name=${name}`,
pendingUserToken,
}
} catch {
return { redirectURL: inputs.authFailedRedirect }
}
}