Skip to content

Commit

Permalink
feat: Add Default role assigning while creating and account or signin…
Browse files Browse the repository at this point in the history
…g in [DEV-2905] (#286)

* Add implementation for default role assigning for newcomers

* Small refactoring of the authentication middleware

* Change request body parsing

* Small changes

* Update package-lock.json

* Annotate file

* Add newline

* Small refactoring and make review comments

* Merge with develop and small improvements

* Presentations are not guarded

* Add newline character

* Fix example

* Remove credential-status/search from guarded paths

* Fix resource create path

* Update swagger.json

* Added instructions in README

* Add newline

* Update package-lock.json

---------

Co-authored-by: Ankur Banerjee <[email protected]>
  • Loading branch information
Andrew Nikitin and ankurdotb authored Jul 9, 2023
1 parent e103242 commit c99bed2
Show file tree
Hide file tree
Showing 21 changed files with 378 additions and 174 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ By default, `ENABLE_AUTHENTICATION` is set to off/`false`. To enable external Ve
3. **Machine-to-machine backend APIs**
1. `LOGTO_M2M_APP_ID`: Application ID for machine-to-machine application in LogTo. This is used for elevated management APIs within LogTo.
2. `LOGTO_M2M_APP_SECRET`: Application secret
4. **Miscellaneous**
4. **Default role update using [LogTo webhooks](https://docs.logto.io/next/docs/recipes/webhooks/)**: LogTo supports webhooks to fire of requests to an API when it detects certain actions/changes. If you want to automatically assign a role to users, a webhook is recommended to be setup for firing off whenever there's a new account created, or a new sign-in.
1. `LOGTO_DEFAULT_ROLE_ID`: LogTo Role ID for the default role to put new users into.
2. `LOGTO_WEBHOOK_SECRET`: Webhook secret to authenticate incoming webhook requests from LogTo.
5. **Miscellaneous**
1. `DEFAULT_CUSTOMER_ID`: Customer/user in LogTo to use for unauthenticated users
2. `COOKIE_SECRET`: Secret for cookie encryption.

Expand Down
4 changes: 4 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ ARG COOKIE_SECRET
ARG LOGTO_M2M_APP_ID
ARG LOGTO_M2M_APP_SECRET
ARG LOGTO_MANAGEMENT_API
ARG LOGTO_DEFAULT_ROLE_ID
ARG LOGTO_WEBHOOK_SECRET

# Verida connector: build-time
ARG ENABLE_VERIDA_CONNECTOR=false
Expand Down Expand Up @@ -103,6 +105,8 @@ ENV COOKIE_SECRET ${COOKIE_SECRET}
ENV LOGTO_M2M_APP_ID ${LOGTO_M2M_APP_ID}
ENV LOGTO_M2M_APP_SECRET ${LOGTO_M2M_APP_SECRET}
ENV LOGTO_MANAGEMENT_API ${LOGTO_MANAGEMENT_API}
ENV LOGTO_DEFAULT_ROLE_ID ${LOGTO_DEFAULT_ROLE_ID}
ENV LOGTO_WEBHOOK_SECRET ${LOGTO_WEBHOOK_SECRET}

# Environment variables: Verida connector
ENV ENABLE_VERIDA_CONNECTOR ${ENABLE_VERIDA_CONNECTOR}
Expand Down
2 changes: 2 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ LOGTO_APP_SECRET='sdf...sdf'
LOGTO_M2M_APP_ID="aaaa...ddddd"
LOGTO_M2M_APP_SECRET="aaaa...ddddd"
LOGTO_MANAGEMENT_API="https://default.logto.app/api"
LOGTO_DEFAULT_ROLE_ID="sdf...sdf"
LOGTO_WEBHOOK_SECRET="sdf...sdf"
COOKIE_SECRET='sdf...sdf'

# Authentication
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 25 additions & 16 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ import swaggerJSONDoc from './static/swagger.json' assert { type: "json" }
import * as dotenv from 'dotenv'
dotenv.config()

import { UserInfo } from './controllers/user_info.js'
import path from 'path'
import e from 'express'
import { LogToWebHook } from './middleware/hook.js'
import { Middleware } from './middleware/middleware.js'

let swagger_options = {}
Expand All @@ -42,8 +41,10 @@ class App {
}

private middleware() {
const auth = new Authentication()
this.express.use(express.json({ limit: '50mb' }))
this.express.use(express.urlencoded({ extended: true }))
this.express.use(express.raw({ type: 'application/octet-stream' }))
this.express.use(express.urlencoded({ extended: true }))
this.express.use(Middleware.parseUrlEncodedJson)
this.express.use(Helmet())
this.express.use(cors({
Expand All @@ -60,12 +61,13 @@ class App {
this.express.use(cookieParser())
if (process.env.ENABLE_AUTHENTICATION === 'true') {
this.express.use(session({secret: process.env.COOKIE_SECRET, cookie: { maxAge: 14 * 24 * 60 * 60 }}))
// Authentication funcitons/methods
this.express.use(Authentication.wrapperHandleAuthRoutes)
this.express.use(Authentication.withLogtoWrapper)
}
if (process.env.ENABLE_EXTERNAL_DB === 'true') {
this.express.use(Authentication.guard)
// Authentication functions/methods
this.express.use(async (req, res, next) => await auth.setup(req, res, next))
this.express.use(async (req, res, next) => await auth.wrapperHandleAuthRoutes(req, res, next))
this.express.use(async (req, res, next) => await auth.withLogtoWrapper(req, res, next))
if (process.env.ENABLE_EXTERNAL_DB === 'true') {
this.express.use(async (req, res, next) => await auth.guard(req, res, next))
}
}
this.express.use(express.text())

Expand All @@ -76,17 +78,17 @@ class App {
return res.send(swaggerUi.generateHTML(swaggerJSONDoc, swagger_options))
}
)
this.express.use(Authentication.handleError)
this.express.use(Authentication.accessControl)
this.express.use(auth.handleError)
this.express.use(async (req, res, next) => await auth.accessControl(req, res, next))
}

private routes() {
const app = this.express

// Top-level routes
app.get('/', (req, res) => res.redirect('swagger'))

app.get('/user', new UserInfo().getUserInfo)

// credentials
// Credential API
app.post(`/credential/issue`, CredentialController.issueValidator, new CredentialController().issue)
app.post(`/credential/verify`, CredentialController.credentialValidator, new CredentialController().verify)
app.post(`/credential/revoke`, CredentialController.credentialValidator, new CredentialController().revoke)
Expand All @@ -106,19 +108,26 @@ class App {
app.post(`/store`, new StoreController().set)
app.get(`/store/:id`, new StoreController().get)

// issuer
// Keys API
app.post(`/key/create`, new IssuerController().createKey)
app.get(`/key/:kid`, new IssuerController().getKey)

// DIDs API
app.post(`/did/create`, IssuerController.createValidator, new IssuerController().createDid)
app.post(`/did/update`, IssuerController.updateValidator, new IssuerController().updateDid)
app.post(`/did/deactivate/:did`, IssuerController.deactivateValidator, new IssuerController().deactivateDid)
app.get(`/did/list`, new IssuerController().getDids)
app.get(`/did/:did`, new IssuerController().getDids)

// Resource API
app.post(`/resource/create/:did`, IssuerController.resourceValidator, new IssuerController().createResource)

// customer
// Account API
app.post(`/account`, new CustomerController().create)
app.get(`/account`, new CustomerController().get)

// LogTo webhooks
app.post(`/account/set-default-role`, LogToWebHook.verifyHookSignature, new CustomerController().setupDefaultRole)

// static files
app.get('/static/custom-button.js',
Expand Down
20 changes: 19 additions & 1 deletion src/controllers/customer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Request, Response } from 'express'

import { CustomerService } from '../services/customer.js'
import { LogToHelper } from '../middleware/auth/logto.js'

export class CustomerController {

Expand Down Expand Up @@ -52,4 +53,21 @@ export class CustomerController {
})
}
}
}

public async setupDefaultRole(request: Request, response: Response) {
if (request.body) {
const body = JSON.parse(request.body)
if (!body.user.isSuspended) {
const logToHelper = new LogToHelper()
await logToHelper.setup()
const resp = await logToHelper.setDefaultRoleForUser(body.user.id as string)
if (resp) {
return response.status(resp.status).json({
error: resp.error})
}
return response.status(500).json({})
}
}
return response.status(400).json({})
}
}
18 changes: 0 additions & 18 deletions src/controllers/user_info.ts

This file was deleted.

8 changes: 8 additions & 0 deletions src/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import pkg from 'secp256k1'
import { fromString } from 'uint8arrays'

import { SpecValidationResult } from '../types/types.js'
import { createHmac } from 'node:crypto'

export function validateSpecCompliantPayload(didDocument: DIDDocument): SpecValidationResult {
// id is required, validated on both compile and runtime
Expand Down Expand Up @@ -70,6 +71,13 @@ export function getCosmosAccount(kid: string) {
return toBech32('cheqd', rawSecp256k1PubkeyToRawAddress(publicKeyConvert(fromString(kid, 'hex'), true)))
}

export function verifyHookSignature(signingKey: string, rawBody: Buffer, expectedSignature: string): boolean {
const hmac = createHmac('sha256', signingKey);
hmac.update(rawBody);
const signature = hmac.digest('hex');
return signature === expectedSignature;
};

export interface IDidDocOptions {
verificationMethod: VerificationMethods
verificationMethodId: any
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/auth/account_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export class AccountAuthHandler extends AbstractAuthHandler {
return this.commonPermissionCheck(request)
}

}
}
16 changes: 9 additions & 7 deletions src/middleware/auth/base_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler
private namespace: Namespaces
private token: string
private scopes: string[] | unknown
private logToHelper?: LogToHelper
private logToHelper: LogToHelper

public customer_id: string

private routeToScoupe: MethodToScope[] = []
private static pathSkip = ['/swagger', '/user', '/static', '/logto']
private static pathSkip = ['/swagger', '/user', '/static', '/logto', '/account/set-default-role']
// private static regExpSkip = new RegExp("^/.*js")

constructor () {
Expand All @@ -40,9 +40,7 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler
this.token = '' as string
this.scopes = undefined
this.customer_id = '' as string
if (process.env.ENABLE_AUTHENTICATION === 'true') {
this.logToHelper = new LogToHelper()
}
this.logToHelper = new LogToHelper()
}

public async commonPermissionCheck(request: Request): Promise<IAuthResponse> {
Expand Down Expand Up @@ -184,7 +182,7 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler
if (!token) {
return {
status: 401,
error: `Unauthorized error: Looks like you are not logged in using LogTo properly.`,
error: `Unauthorized error: Looks like you are not logged in using LogTo properly or don't have needed permissions.`,
data: {
customerId: '',
scopes: [],
Expand All @@ -198,6 +196,10 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler
}

// common utils
public setLogToHelper(logToHelper: LogToHelper) {
this.logToHelper = logToHelper
}

public static getNamespaceFromRequest(req: Request): Namespaces {
const matches = stringify(req.body).match(cheqdDidRegex)
if (matches && matches.length > 0) {
Expand Down Expand Up @@ -300,4 +302,4 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler
}
return false
}
}
}
21 changes: 0 additions & 21 deletions src/middleware/auth/credential-status.ts

This file was deleted.

23 changes: 23 additions & 0 deletions src/middleware/auth/credential-status_auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Request, Response } from "express";
import { AbstractAuthHandler } from "./base_auth.js";
import { IAuthResponse } from "../../types/authentication.js";

export class CredentialStatusAuthHandler extends AbstractAuthHandler {

constructor () {
super()
this.registerRoute('/credential-status/create', 'POST', 'create:credential-status:testnet')
this.registerRoute('/credential-status/create', 'POST', 'create:credential-status:mainnet')
this.registerRoute('/credential-status/publish', 'POST', 'publish:credential-status:testnet')
this.registerRoute('/credential-status/publish', 'POST', 'publish:credential-status:mainnet')
this.registerRoute('/credential-status/update', 'POST', 'update:credential-status:testnet')
this.registerRoute('/credential-status/update', 'POST', 'update:credential-status:mainnet')
}
public async handle(request: Request, response: Response): Promise<IAuthResponse> {
if (!request.path.includes('/credential-status')) {
return super.handle(request, response)
}
return this.commonPermissionCheck(request)
}

}
8 changes: 3 additions & 5 deletions src/middleware/auth/credential_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@ export class CredentialAuthHandler extends AbstractAuthHandler {
super()
this.registerRoute('/credential/issue', 'POST', 'issue:credential:testnet')
this.registerRoute('/credential/issue', 'POST', 'issue:credential:mainnet')
this.registerRoute('/credential/verify', 'POST', 'verify:credential:testnet')
this.registerRoute('/credential/verify', 'POST', 'verify:credential:mainnet')
this.registerRoute('/credential/revoke', 'POST', 'revoke:credential:testnet')
this.registerRoute('/credential/revoke', 'POST', 'revoke:credential:mainnet')
this.registerRoute('/credential/suspend', 'POST', 'suspend:credential:testnet')
this.registerRoute('/credential/suspend', 'POST', 'suspend:credential:mainnet')
this.registerRoute('/credential/unsuspend', 'POST', 'unsuspend:credential:testnet')
this.registerRoute('/credential/unsuspend', 'POST', 'unsuspend:credential:mainnet')
this.registerRoute('/credential/reinstate', 'POST', 'reinstate:credential:testnet')
this.registerRoute('/credential/reinstate', 'POST', 'reinstate:credential:mainnet')
}

public async handle(request: Request, response: Response): Promise<IAuthResponse>{
Expand All @@ -25,4 +23,4 @@ export class CredentialAuthHandler extends AbstractAuthHandler {
return this.commonPermissionCheck(request)
}

}
}
5 changes: 3 additions & 2 deletions src/middleware/auth/did_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export class DidAuthHandler extends AbstractAuthHandler {
this.registerRoute('/did', 'GET', 'read:did:mainnet')
this.registerRoute('/did/update', 'POST', 'update:did:testnet')
this.registerRoute('/did/update', 'POST', 'update:did:mainnet')

this.registerRoute('/did/deactivate', 'POST', 'update:did:testnet')
this.registerRoute('/did/deactivate', 'POST', 'update:did:mainnet')
}

public async handle(request: Request, response: Response): Promise<IAuthResponse> {
Expand All @@ -24,4 +25,4 @@ export class DidAuthHandler extends AbstractAuthHandler {
return this.commonPermissionCheck(request)
}

}
}
2 changes: 1 addition & 1 deletion src/middleware/auth/key_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ export class KeyAuthHandler extends AbstractAuthHandler {

}

}
}
Loading

0 comments on commit c99bed2

Please sign in to comment.