Skip to content

Commit

Permalink
fix: Support sign in with apple without arc
Browse files Browse the repository at this point in the history
When we dropped arc middlware it stopped automatically handling the base64 encoded post body. Now looking for it and handling it.
  • Loading branch information
activescott committed Feb 1, 2021
1 parent ceec592 commit 59ef838
Show file tree
Hide file tree
Showing 10 changed files with 66 additions and 28 deletions.
21 changes: 21 additions & 0 deletions server/src/shared/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
type BufferEncoding =
| "ascii"
| "utf8"
| "utf-8"
| "utf16le"
| "ucs2"
| "ucs-2"
| "base64"
| "latin1"
| "binary"
| "hex"

/**
* From base64 to the specified target encoding.
*/
export function fromBase64(
base64Encoded: string,
targetEncoding: BufferEncoding = "utf-8"
): string {
return Buffer.from(base64Encoded, "base64").toString(targetEncoding)
}
2 changes: 0 additions & 2 deletions server/src/shared/lambda/middleware/csrf.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ describe("csrf", () => {
})

it("should require response", async () => {
const res: LambdaHttpResponse = {}
expect(
addCsrfTokenToResponse("foo", (null as unknown) as LambdaHttpResponse)
).rejects.toThrowError(/response/)
Expand Down Expand Up @@ -74,7 +73,6 @@ describe("csrf", () => {

sinon.verifyAndRestore()
})

})

describe("request middleware", () => {
Expand Down
11 changes: 4 additions & 7 deletions server/src/shared/lambda/middleware/csrf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,13 @@ export function isTokenValid(token: string, sessionID: string): boolean {
// our CSRF token has the session id in it. Now that we've validated the token, extract the session id and make sure that it matches
const csrfSessionID = ater.getTokenValue(token)
if (csrfSessionID != sessionID) {
warn(
"CSRF token does not match session:",
csrfSessionID,
"!=",
sessionID
)
warn("CSRF token does not match session:", csrfSessionID, "!=", sessionID)
return false
}
return true
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function warn(message?: any, ...optionalParams: any[]): void {
if (!("CSRF_TOKEN_WARNING_DISABLE" in process.env)) {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -96,8 +92,9 @@ const createTokenater = (): Tokenater =>
new Tokenater(getSecret(), Tokenater.DAYS_IN_MS * 1)

function getSecret(): string {
const KEY_LENGTH = 32
return secretFromEnvironment(
"WAS_CSRF_SECRET",
`${process.env.NODE_ENV}`
).padEnd(32, ".")
).padEnd(KEY_LENGTH, ".")
}
7 changes: 6 additions & 1 deletion server/src/shared/lambda/middleware/session.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { writeSessionID, readSessionID, UserSession, createAnonymousSessionID } from "./session"
import {
writeSessionID,
readSessionID,
UserSession,
createAnonymousSessionID,
} from "./session"
import { createMockRequest } from "../../../../test/support/lambda"

describe("session", () => {
Expand Down
30 changes: 22 additions & 8 deletions server/src/shared/lambda/middleware/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@ import {
} from "cookie"
import { LambdaHttpRequest, LambdaHttpResponse } from "../../lambda"
import { assert } from "console"
import { daysToSeconds, millisecondsToSeconds, secondsToMilliseconds } from "../../time"
import {
daysToSeconds,
millisecondsToSeconds,
secondsToMilliseconds,
} from "../../time"
import { secretFromEnvironment } from "../../secretEnvironment"
import * as jwt from "node-webtokens"

/** The name of the session key to get the session ID value.
* Exported ONLY FOR TESTING.
*/
export const SESSION_COOKIE_NAME = "WAS_SES"
const SESSION_EXPIRATION_DURATION_IN_SECONDS = daysToSeconds(30)
const SESSION_EXPIRATION_DURATION_IN_DAYS = 30
const SESSION_EXPIRATION_DURATION_IN_SECONDS = daysToSeconds(
SESSION_EXPIRATION_DURATION_IN_DAYS
)

type HttpResponseLike = Pick<LambdaHttpResponse, "cookies">
type HttpRequestLike = Pick<LambdaHttpRequest, "cookies">
Expand Down Expand Up @@ -102,7 +109,8 @@ function jwtSecret(): string {
"WAS_SESSION_SECRET",
`${process.env.NODE_ENV}`
)
const key = secret.padEnd(32, ".")
const KEY_LENGTH = 32
const key = secret.padEnd(KEY_LENGTH, "_")
// NOTE: node-webtokens requires it to be in base64 format:
return Buffer.from(key, "utf-8").toString("base64")
}
Expand All @@ -125,7 +133,9 @@ function sessionToJWT(session: UserSession): string {
{
iss: JWT_ISSUER,
aud: JWT_AUDIENCE,
exp: millisecondsToSeconds(Date.now()) + SESSION_EXPIRATION_DURATION_IN_SECONDS,
exp:
millisecondsToSeconds(Date.now()) +
SESSION_EXPIRATION_DURATION_IN_SECONDS,
sub: session.userID,
iat: session.createdAt,
},
Expand All @@ -134,7 +144,6 @@ function sessionToJWT(session: UserSession): string {
}

function jwtToSession(jwtString: string): UserSession | null {
// TODO:
const parsed = jwt
.parse(jwtString)
.setIssuer([JWT_ISSUER])
Expand All @@ -143,12 +152,14 @@ function jwtToSession(jwtString: string): UserSession | null {

// See https://www.npmjs.com/package/node-webtokens
if (parsed.error) {
// eslint-disable-next-line no-console
console.error(
"unable to deserialize session:" + JSON.stringify(parsed.error, null, 2)
"unable to deserialize session:" + JSON.stringify(parsed.error)
)
return null
} else if (parsed.expired) {
console.error("session token expired")
// eslint-disable-next-line no-console
console.warn("session token expired")
return null
}

Expand All @@ -157,6 +168,7 @@ function jwtToSession(jwtString: string): UserSession | null {
(claim) => !(claim in parsed.payload)
)
if (missingClaims.length > 0) {
// eslint-disable-next-line no-console
console.error(
"the following claims are missing in session jwt:",
missingClaims
Expand All @@ -170,7 +182,9 @@ function jwtToSession(jwtString: string): UserSession | null {
}

function toCookie(name: string, value: UserSession): string {
const expires = new Date(Date.now() + secondsToMilliseconds(SESSION_EXPIRATION_DURATION_IN_SECONDS))
const expires = new Date(
Date.now() + secondsToMilliseconds(SESSION_EXPIRATION_DURATION_IN_SECONDS)
)
const options: CookieSerializeOptions = {
expires,
httpOnly: true,
Expand Down
5 changes: 4 additions & 1 deletion server/src/shared/lambda/oauth/handlers/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export default function loginHandlerFactory(
if (!userRepository) throw new Error("userRepository must be specified")

// the handler:
async function loginHandler(req: LambdaHttpRequest): Promise<LambdaHttpResponse> {
async function loginHandler(
req: LambdaHttpRequest
): Promise<LambdaHttpResponse> {
/**
* This is where we start the login flow. Uses the following steps:
* 1. Get the provider from query string (?provider=<provider name>)
Expand Down Expand Up @@ -74,6 +76,7 @@ export default function loginHandlerFactory(
if (session) {
const user = await userRepository.get(session.userID)
if (!user) {
// eslint-disable-next-line no-console
console.warn(
"login: No user found for session",
session,
Expand Down
7 changes: 1 addition & 6 deletions server/src/shared/lambda/oauth/handlers/me.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,7 @@ describe("me handler", () => {

expect(response.body).toBeTruthy()
const bodyJson = JSON.parse(response.body as string)
const expectedProps = [
"sub",
"createdAt",
"updatedAt",
"providers",
]
const expectedProps = ["sub", "createdAt", "updatedAt", "providers"]
expectedProps.forEach((prop) => expect(bodyJson).toHaveProperty(prop))
})

Expand Down
7 changes: 5 additions & 2 deletions server/src/shared/lambda/oauth/handlers/redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
LambdaHttpResponse,
} from "../../../lambda"
import { secondsToMilliseconds } from "../../../time"
import { fromBase64 } from "../../../encoding"

/**
* Factory to create a handler for the [Authorization Response](https://tools.ietf.org/html/rfc6749#section-4.1.2) when the user is directed with a `code` from the OAuth Authorization Server back to the OAuth client application.
Expand Down Expand Up @@ -181,9 +182,11 @@ function parseParameters(req: LambdaHttpRequest): OAuthResponseParameters {
}
} else if (method.toUpperCase() === "POST") {
// form_post response_mode per https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html
const parsed = new URLSearchParams(req.body)
if (!req.body) throw new Error("post body is empty")
const body = req.isBase64Encoded ? fromBase64(req.body) : req.body
const parsed = new URLSearchParams(body)
// eslint-disable-next-line no-console
console.log("redirect params (POST):", parsed)
console.log("redirect params (POST):", parsed.toString())
return {
error: parsed.get("error"),
code: parsed.get("code"),
Expand Down
2 changes: 2 additions & 0 deletions server/src/shared/time.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable no-magic-numbers */
const MS_PER_SECOND = 1000
const SECONDS_PER_HOUR = 60 * 60
const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
/* eslint-enable no-magic-numbers */

type Seconds = number
type Milliseconds = number
Expand Down
2 changes: 1 addition & 1 deletion server/test/support/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ process.env.WAS_CSRF_SECRET = "jest-secret"
process.env.SESSION_TOKEN_SECRET = "jest-secret"
process.env.CSRF_TOKEN_WARNING_DISABLE = ""
process.env.TOKENATER_WARNING_DISABLE = ""
process.env.WAS_SESSION_SECRET = "test-secret"
process.env.WAS_SESSION_SECRET = "test-secret"

0 comments on commit 59ef838

Please sign in to comment.