Skip to content

Security: ivank/laminar

Security

docs/security.md

Implementing security for OpenAPI Schema

If you have this schema, that has JWT Security schema and uses it to protect the get method without any scopes:

examples/security/src/api.yaml

openapi: '3.0.0'
info:
  title: 'Security Example'
  version: 1.0.0
paths:
  /user/{id}:
    get:
      # Using the JWT security, with no scopes
      security: [{ JWT: [] }]
      parameters:
        - name: 'id'
          in: 'path'
          required: true
          schema:
            type: 'string'
            pattern: '\d+'
      responses:
        200:
          description: 'User'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserResponse'
components:
  securitySchemes:
    # Defining the JWT security schema
    JWT:
      type: http
      scheme: bearer
  schemas:
    UserResponse:
      type: object
      properties:
        id:
          type: string
        name:
          type: string

You can then implement the security, by writing a Security Resolver function, that would return either securityOk or any Response object. If the user is authenticated, then laminar will add the user auth info in the userAuth property of hte request, otherwise it would just return the Response object, whatever it is.

examples/security/src/index.ts

import { HttpService, init, jsonOk, securityOk, securityError } from '@laminarjs/laminar';
import { join } from 'path';
import { openApiTyped } from './__generated__/api';

const findUser = (id: string) => ({ id, name: 'John' });

/**
 * A simple validate function that would return either SecurityOk or Response objects.
 * All security resolvers must do that.
 * If you return `securityOk` that means the user is validated
 * and the contents of `securityOk` would be passed to the `authInfo` property of the object
 *
 * Otherwise the Response would be returned directly
 */
const validate = (authorizaitonHeader?: string) =>
  authorizaitonHeader === 'Secret Pass'
    ? securityOk({ email: 'me@example.com' })
    : securityError({ message: 'Unkown user' });

const main = async () => {
  const listener = await openApiTyped({
    api: join(__dirname, 'api.yaml'),
    security: {
      /**
       * Implementing the security
       */
      JWT: ({ headers }) => validate(headers.authorization),
    },
    paths: {
      '/user/{id}': {
        get: async ({ path }) => jsonOk(findUser(path.id)),
      },
    },
  });
  const http = new HttpService({ listener });
  await init({ initOrder: [http], logger: console });
};

main();

JWT Security

If we had this oapi.yaml

packages/jwt/examples/oapi.yaml

---
openapi: 3.0.0
info:
  title: Test
  version: 1.0.0
servers:
  - url: http://localhost:3333
paths:
  '/session':
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/User' }
      responses:
        '200':
          description: A session object
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Session' }
  '/test':
    post:
      security:
        # Using the JWTSecurity with admin scopes
        - JWTSecurity: ['admin']
      responses:
        '200':
          description: A Test Object
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Test' }
    get:
      security:
        # Using the JWTSecurity with no scopes
        - JWTSecurity: []
      responses:
        '200':
          description: A Test Object
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Test' }

components:
  securitySchemes:
    # Defining the JWTSecurity schema to be used on the routes
    JWTSecurity:
      type: http
      scheme: bearer

  schemas:
    Session:
      additionalProperties: false
      properties:
        jwt:
          type: string
        user:
          $ref: '#/components/schemas/User'
      required:
        - jwt
        - user
    User:
      properties:
        email:
          type: string
        scopes:
          type: array
          items:
            type: string
      required:
        - email

    Test:
      properties:
        text:
          type: string
        user:
          $ref: '#/components/schemas/User'
      required:
        - text

And then implement it using the helper jwtSecurityResolver. That function would return a securityOk object if the jwt was validated, with the contents of the jwt, or a 403 error response.

packages/jwt/examples/oapi.ts

import { HttpService, init, jsonOk, openApi } from '@laminarjs/laminar';
import { createSession, jwtSecurityResolver } from '@laminarjs/jwt';
import { join } from 'path';

const main = async () => {
  const secret = '123';
  const listener = await openApi({
    api: join(__dirname, 'oapi.yaml'),
    security: { JWTSecurity: jwtSecurityResolver({ secret }) },
    paths: {
      '/session': {
        post: async ({ body }) => jsonOk(createSession({ secret }, body)),
      },
      '/test': {
        get: async ({ authInfo }) => jsonOk({ text: 'ok', user: authInfo }),
        post: async ({ authInfo }) => jsonOk({ text: 'ok', user: authInfo }),
      },
    },
  });
  const http = new HttpService({ listener });
  await init({ initOrder: [http], logger: console });
};

main();

Cookie Security

If you need the old school but still awesome cookie security, OpenAPI can handle that too - docs for cookie auth with OpenAPI. You can use the "apiKey" security to define it.

packages/jwt/examples/oapi-api-key.yaml

---
openapi: 3.0.0
info:
  title: Test
  version: 1.0.0
servers:
  - url: http://localhost:3333
paths:
  '/session':
    post:
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/User' }
      responses:
        '200':
          description: A session object
          content:
            text/plain:
              schema: { $ref: '#/components/schemas/Text' }
          headers:
            Set-Cookie:
              schema:
                type: string
                example: auth=abcde12345; Path=/; HttpOnly
  '/test':
    post:
      description: Protected by CookieSecurity, no scopes
      security:
        - CookieSecurity: []
      responses:
        '200':
          description: A Test Object
          content:
            text/plain:
              schema: { $ref: '#/components/schemas/Text' }
    get:
      description: Protected by CookieSecurity, no scopes
      security:
        - CookieSecurity: []
      responses:
        '200':
          description: A Test Object
          content:
            text/plain:
              schema: { $ref: '#/components/schemas/Text' }

components:
  securitySchemes:
    CookieSecurity:
      description: Security using the `auth` cookie. To be used in the routes.
      type: apiKey
      in: cookie
      name: auth

  schemas:
    User:
      properties:
        email:
          type: string
      required:
        - email
    Text:
      type: string

Implementing it involves reading the cookie and validating its contents.

packages/jwt/examples/oapi-api-key.ts

import { HttpService, init, openApi, textOk, setCookie } from '@laminarjs/laminar';
import { createSession, verifyToken } from '@laminarjs/jwt';
import { join } from 'path';

const main = async () => {
  const secret = '123';
  const listener = await openApi({
    api: join(__dirname, 'oapi-api-key.yaml'),
    security: {
      /**
       * Implement cookie security.
       */
      CookieSecurity: ({ cookies, scopes }) => verifyToken({ secret }, cookies?.auth, scopes),
    },
    paths: {
      '/session': {
        post: async ({ body }) => setCookie({ auth: createSession({ secret }, body).jwt }, textOk('Cookie Set')),
      },
      '/test': {
        get: async () => textOk('OK'),
        post: async ({ authInfo }) => textOk(`OK ${authInfo.email}`),
      },
    },
  });
  const http = new HttpService({ listener });
  await init({ initOrder: [http], logger: console });
};

main();

Custom security resolvers

OpenApi supports more security methods, and they can be implemented with a security resolver. Since a security resolver is just a function that gets request properties and returns either securityOk or a Response object, we can do a lot of custom things.

packages/jwt/examples/oapi-custom.yaml

---
openapi: 3.0.0
info:
  title: Test
  version: 1.0.0
servers:
  - url: http://localhost:3333
paths:
  '/session':
    post:
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: '#/components/schemas/User' }
      responses:
        '200':
          description: A session object
          content:
            text/plain:
              schema: { $ref: '#/components/schemas/Text' }
          headers:
            Set-Cookie:
              schema:
                type: string
                example: auth=abcde12345; Path=/; HttpOnly
  '/test':
    post:
      description: Either CookieSecurity or CloudSchedulerSecurity should match, no scopes
      security:
        - CookieSecurity: []
        - CloudSchedulerSecurity: []
      responses:
        '200':
          description: A Test Object
          content:
            text/plain:
              schema: { $ref: '#/components/schemas/Text' }
    get:
      description: Only CookieSecurity can be used for this route, no scopes, no scopes
      security:
        - CookieSecurity: []
      responses:
        '200':
          description: A Test Object
          content:
            text/plain:
              schema: { $ref: '#/components/schemas/Text' }
  '/unauthorized':
    get:
      responses:
        '403':
          description: Forbidden
          content:
            text/plain:
              schema: { $ref: '#/components/schemas/Text' }

components:
  securitySchemes:
    CookieSecurity:
      description: Security using the `auth` cookie. To be used in the routes.
      type: apiKey
      in: cookie
      name: auth
    CloudSchedulerSecurity:
      description: Security using the `X-CloudScheduler` header. To be used in the routes.
      type: apiKey
      in: header
      name: 'x-cloudscheduler'

  schemas:
    User:
      properties:
        email:
          type: string
      required:
        - email
    Text:
      type: string

packages/jwt/examples/oapi-custom.ts

import {
  HttpService,
  init,
  openApi,
  securityRedirect,
  isSecurityOk,
  securityOk,
  textOk,
  textForbidden,
  setCookie,
  securityError,
} from '@laminarjs/laminar';
import { createSession, verifyToken } from '@laminarjs/jwt';
import { join } from 'path';

const main = async () => {
  const secret = '123';
  const listener = await openApi({
    api: join(__dirname, 'oapi-custom.yaml'),
    security: {
      /**
       * Implement additional cookie security.
       * In an event of a failure, we'd want to redirect to an error page, instead of returning a 403 response
       */
      CookieSecurity: async ({ cookies, scopes }) => {
        const result = await verifyToken({ secret }, cookies?.auth, scopes);
        return isSecurityOk(result) ? result : securityRedirect('/unauthorized', { body: { message: 'Redirect' } });
      },
      /**
       * Cloud Scheduler would ensure that this header is never sent outside of the app engine environment,
       * so we're safe just checking for the existance of the header.
       */
      CloudSchedulerSecurity: ({ headers }) =>
        headers['x-cloudscheduler'] ? securityOk({}) : securityError({ message: 'Not Cloud Scheduler Job' }),
    },
    paths: {
      '/session': {
        post: async ({ body }) => setCookie({ auth: createSession({ secret }, body).jwt }, textOk('Cookie Set')),
      },
      '/test': {
        get: async () => textOk('OK'),
        post: async ({ authInfo }) => textOk(`OK ${authInfo.email}`),
      },
      '/unauthorized': { get: async () => textForbidden('Forbidden!') },
    },
  });
  const http = new HttpService({ listener });
  await init({ initOrder: [http], logger: console });
};

main();

There aren’t any published security advisories