Skip to content

Commit

Permalink
feat: GET /me now returns a providers attribute listing the identity …
Browse files Browse the repository at this point in the history
…providers user has authenticated with
  • Loading branch information
activescott committed Jan 18, 2021
1 parent 03c2ef4 commit 0f29b29
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 14 deletions.
6 changes: 5 additions & 1 deletion server/src/http/get-auth-me/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as arc from "@architect/functions"
import userRepositoryFactory from "@architect/shared/architect/oauth/repository/UserRepository"
import tokenRepositoryFactory from "@architect/shared/architect/oauth/repository/TokenRepository"
import meHandlerFactory from "@architect/shared/architect/oauth/handlers/me"

const handlerImp = meHandlerFactory(userRepositoryFactory())
const handlerImp = meHandlerFactory(
userRepositoryFactory(),
tokenRepositoryFactory()
)
export const handler = arc.http.async(handlerImp)
2 changes: 1 addition & 1 deletion server/src/http/get-auth-redirect-000provider/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as arc from "@architect/functions"
import oAuthRedirectHandlerFactory from "@architect/shared/architect/oauth/handlers/redirect"
import { tokenRepositoryFactory } from "@architect/shared/architect/oauth/repository/TokenRepository"
import tokenRepositoryFactory from "@architect/shared/architect/oauth/repository/TokenRepository"
import userRepositoryFactory from "@architect/shared/architect/oauth/repository/UserRepository"
import { fetchJson } from "@architect/shared/fetch"

Expand Down
34 changes: 28 additions & 6 deletions server/src/shared/architect/oauth/handlers/me.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { HttpHandler, HttpRequest, HttpResponse } from "@architect/functions"
import { map } from "irritable-iterable"
import { readSessionID } from "../../middleware/session"
import { TokenRepository } from "../repository/TokenRepository"
import { StoredUser, UserRepository } from "../repository/UserRepository"

import * as STATUS from "./httpStatus"
Expand All @@ -9,36 +11,56 @@ import * as STATUS from "./httpStatus"
* @param req The incoming Architect/APIG/Lambda request.
*/
export default function meHandlerFactory(
userRepository: UserRepository
userRepository: UserRepository,
tokenRepository: TokenRepository
): HttpHandler {
async function handlerImp(
req: HttpRequest
): Promise<HttpResponse> {
async function handlerImp(req: HttpRequest): Promise<HttpResponse> {
const sessionID = readSessionID(req)
if (!sessionID) {
return {
statusCode: STATUS.UNAUTHENTICATED,
json: {
error: "request not authenticated",
},
}
}
const user = await userRepository.get(sessionID)
if (!user) {
return {
statusCode: STATUS.NOT_FOUND,
json: {
error: "not found",
error: "user not found",
},
}
}
// we try to be compliant with the OIDC UserInfo Response:

// we try to be compliant with the OIDC UserInfo Response: https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
return {
statusCode: STATUS.OK,
json: {
sub: user.id,
email: user.email,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
...(await getProviders(user)),
},
}
}
return handlerImp

async function getProviders(
user: StoredUser
): Promise<{ providers: string[] }> {
try {
const tokens = await tokenRepository.listForUser(user.id)
const providers: string[] = map(tokens, (t) => t.provider).collect()
return {
providers,
}
} catch (err) {
// providers are non-essential so rather than fail, just log it and return empty providers
console.error(err)
return { providers: [] }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
readSessionID,
writeSessionID,
} from "../../middleware/session"
import { tokenRepositoryFactory } from "../repository/TokenRepository"
import tokenRepositoryFactory from "../repository/TokenRepository"
import userRepositoryFactory from "../repository/UserRepository"
import oAuthRedirectHandlerFactory from "./redirect"
import * as jwt from "node-webtokens"
Expand Down
4 changes: 2 additions & 2 deletions server/src/shared/architect/oauth/repository/Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default abstract class Repository<T extends StoredItem> {
await this.ensureInitialized()
try {
const now = Date.now()
// NOTE: explicitly NOT modifying the passed-in user obj
// NOTE: explicitly NOT modifying the passed-in obj
const storedItem: T = {
...proposedItem,
createdAt: now,
Expand Down Expand Up @@ -87,7 +87,7 @@ export default abstract class Repository<T extends StoredItem> {
// TODO: need to fix this. See Alert Genie for some examples of doing this more cleanly with an Iterable.
assert(
!scanned.LastEvaluatedKey,
"LastEvaluatedKey not empty. More users must exist and paging isn't implemented!"
"LastEvaluatedKey not empty. More items must exist and paging isn't implemented!"
)
return scanned.Items as T[]
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { first } from "irritable-iterable"
import { randomInt } from "../../../../../test/support"
import {
import tokenRepositoryFactory, {
StoredToken,
StoredTokenProposal,
TokenRepository,
tokenRepositoryFactory,
} from "./TokenRepository"

let repo: TokenRepository
Expand Down Expand Up @@ -64,6 +64,23 @@ describe("get", () => {
})
})

describe("listForUser", () => {
it("should return providers", async () => {
const proposed = randomToken()
await repo.upsert(proposed)
const tokens = await repo.listForUser(proposed.userID)
expect(tokens).toHaveLength(1)
expect(first(tokens)).toHaveProperty("provider", proposed.provider)
})

it("should be empty with no tokens", async () => {
const proposed = randomToken()
// don't add user:
const tokens = await repo.listForUser(proposed.userID)
expect(tokens).toHaveLength(0)
})
})

function expectStrictTokenProps(actual: StoredToken): void {
const expectedProps: Record<string, string> = {
id: "string",
Expand Down
37 changes: 36 additions & 1 deletion server/src/shared/architect/oauth/repository/TokenRepository.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import assert from "assert"
import Repository from "./Repository"
import { StoredItem } from "./StoredItem"

export interface TokenRepository {
upsert(token: StoredTokenProposal): Promise<StoredToken>
get(userID: string, provider: string): Promise<StoredToken>
list(): Promise<Iterable<StoredToken>>
listForUser(userID: string): Promise<Iterable<StoredToken>>
delete(tokenID: string): Promise<void>
}

export function tokenRepositoryFactory(): TokenRepository {
export default function tokenRepositoryFactory(): TokenRepository {
return new TokenRepositoryImpl()
}

Expand Down Expand Up @@ -49,6 +51,35 @@ class TokenRepositoryImpl
return result.Item as StoredToken
}

/**
* Lists all the tokens for the specified userID.
* @param userID
*/
public async listForUser(userID: string): Promise<Iterable<StoredToken>> {
if (!userID || typeof userID !== "string") {
throw new Error("userID argument must be provided and must be a string")
}
const result = await (await this.getDDB())
.scan({
TableName: await this.getTableName(),
FilterExpression: "begins_with(id, :id_prefix)",
ExpressionAttributeValues: {
":id_prefix": this.idPrefix(userID),
},
})
.promise()

if (!result.Items || result.Items.length === 0) {
return []
}
// TODO: need to fix this. See Alert Genie for some examples of doing this more cleanly with an Iterable.
assert(
!result.LastEvaluatedKey,
"LastEvaluatedKey not empty. More items must exist and paging isn't implemented!"
)
return result.Items as StoredToken[]
}

public async list(): Promise<Iterable<StoredToken>> {
return this.listItems()
}
Expand All @@ -60,6 +91,10 @@ class TokenRepositoryImpl
private idForToken(userID: string, provider: string): string {
return `${this.tableNickname}:${userID}#${provider}`
}

private idPrefix(userID: string): string {
return `${this.tableNickname}:${userID}#`
}
}

/**
Expand Down

0 comments on commit 0f29b29

Please sign in to comment.