Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: authentication example #81

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/controllers/handlers/ping-handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { HandlerContextWithPath } from "../../types"

// handlers arguments only type what they need, to make unit testing easier
export async function pingHandler(context: Pick<HandlerContextWithPath<"metrics", "/ping">, "url" | "components">) {
export async function pingHandler(
context: Pick<
HandlerContextWithPath<"metrics", "/ping", "authenticationData">,
"url" | "components" | "authenticationData"
>
) {
const {
url,
components: { metrics },
Expand All @@ -11,6 +16,10 @@ export async function pingHandler(context: Pick<HandlerContextWithPath<"metrics"
pathname: url.pathname,
})

if (context.authenticationData) {
return { body: context.authenticationData.authData.name }
}

return {
body: url.pathname,
}
Expand Down
5 changes: 5 additions & 0 deletions src/controllers/routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Router } from "@well-known-components/http-server"
import { compose } from "@well-known-components/http-server/dist/middleware"
import { basicAuthenticationMiddleware } from "../logic/authentication-middleware"
import { GlobalContext } from "../types"
import { getUserFromToken } from "../logic/getUser"
import { pingHandler } from "./handlers/ping-handler"

// We return the entire router because it will be easier to test than a whole server
Expand All @@ -8,5 +11,7 @@ export async function setupRouter(globalContext: GlobalContext): Promise<Router<

router.get("/ping", pingHandler)

router.get("/secure/ping", compose(basicAuthenticationMiddleware<GlobalContext>(getUserFromToken), pingHandler))

return router
}
42 changes: 42 additions & 0 deletions src/logic/authentication-middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* This file may be a library on its own. It is recommended that if you are
* creating microservices, you may extract this logic to a package that can
* be reused across servers.
*/

import { IHttpServerComponent as h } from "@well-known-components/interfaces"

export type TokenBasedAuthData<T> = {
bearerToken: string
authData: T
}

export function basicAuthenticationMiddleware<Ctx extends { authenticationData?: TokenBasedAuthData<T> }, T = {}>(
getUser: (context: Ctx, bearerToken: string) => Promise<T | null>
): h.IRequestHandler<Ctx> {
return async (ctx, next) => {
const auth = ctx.request.headers.get("authorization")

// mising auth header
if (!auth) return { status: 401 }

const bearerTokenMatch = /^[Bb]earer\s+(.+)$/.exec(auth)

// invalid bearer token
if (!bearerTokenMatch) return { status: 401 }

const bearerToken = bearerTokenMatch[1]

const authData = await getUser(ctx, bearerToken)

// invalid authData
if (!authData) return { status: 401 }

ctx.authenticationData = {
bearerToken,
authData,
}

return next()
}
}
17 changes: 17 additions & 0 deletions src/logic/getUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PickComponents } from "../types"

export type UserData = {
name: string
}

export async function getUserFromToken(_ctx: PickComponents<"config">, token: string): Promise<UserData | null> {
// use ctx.component.config or database or any other component to get the user from the token
if (token == "tester") {
return {
name: "beta-tester",
}
}

// return null or throw in case of failure
return null
}
13 changes: 11 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import type {
IBaseComponent,
IMetricsComponent,
} from "@well-known-components/interfaces"
import { UserData } from "./logic/getUser"
import { TokenBasedAuthData } from "./logic/authentication-middleware"
import { metricDeclarations } from "./metrics"

export type GlobalContext = {
components: BaseComponents
authenticationData?: TokenBasedAuthData<UserData>
}

// components used in every environment
Expand All @@ -35,12 +38,18 @@ export type TestComponents = BaseComponents & {
// this type simplifies the typings of http handlers
export type HandlerContextWithPath<
ComponentNames extends keyof AppComponents,
Path extends string = any
Path extends string = any,
GlobalContextKeys extends keyof GlobalContext = any
> = IHttpServerComponent.PathAwareContext<
IHttpServerComponent.DefaultContext<{
components: Pick<AppComponents, ComponentNames>
}>,
Path
>
> &
Omit<Pick<GlobalContext, GlobalContextKeys>, "components">

export type PickComponents<ComponentNames extends keyof AppComponents, Ctx = {}> = Ctx & {
components: Pick<AppComponents, ComponentNames>
}

export type Context<Path extends string = any> = IHttpServerComponent.PathAwareContext<GlobalContext, Path>