Skip to content

Commit

Permalink
Add implementation for default role assigning for newcomers
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Nikitin committed Jul 6, 2023
1 parent 3a35048 commit b1da7a4
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 41 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ We use a self-hosted version of [LogTo](https://logto.io/), which supports OpenI
6. `LOGTO_M2M_APP_ID`: Machine-to-machine Application ID
7. `LOGTO_M2M_APP_SECRET`: Machine-to-machine Application secret
8. `LOGTO_MANAGEMENT_API`: URL of management API for LogTo (default is `https://default.logto.app/api`)
9. `ALLOWED_ORIGINS`: CORS allowed origins used in the app
10. `DEFAULT_CUSTOMER_ID`: Customer/user in LogTo to use for unauthenticated users
11. `COOKIE_SECRET`: Secret for cookie encryption.
9. `LOGTO_DEFAULT_ROLE`: By default it's "Authenticated User - Testnet". So, it's a name of role with default set of permissions for the user which only created or logged in.
10. `LOGTO_WEBHOOK_SECRET`: WebHook secret which come from LogTo Webhooks page. Needed for checking that web hook was originally sent from LogTo
11. `ALLOWED_ORIGINS`: CORS allowed origins used in the app
12. `DEFAULT_CUSTOMER_ID`: Customer/user in LogTo to use for unauthenticated users
13. `COOKIE_SECRET`: Secret for cookie encryption.

### 3rd Party Connectors

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="Authenticated User - Testnet"
LOGTO_WEBHOOK_SECRET="sdf...sdf"
COOKIE_SECRET='sdf...sdf'

# Authentication
Expand Down
25 changes: 15 additions & 10 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import cors from 'cors'
import swaggerUi from 'swagger-ui-express'
import session from 'express-session'
import cookieParser from 'cookie-parser'
import {withLogto, handleAuthRoutes } from '@logto/express'

import { CredentialController } from './controllers/credentials.js'
import { StoreController } from './controllers/store.js'
Expand All @@ -13,7 +12,7 @@ import { CustomerController } from './controllers/customer.js'
import { Authentication } from './middleware/authentication.js'
import { Connection } from './database/connection/connection.js'
import { RevocationController } from './controllers/revocation.js'
import { CORS_ERROR_MSG, configLogToExpress } from './types/constants.js'
import { CORS_ERROR_MSG } from './types/constants.js'

import swaggerJSONDoc from './static/swagger.json' assert { type: "json" }

Expand All @@ -22,6 +21,7 @@ dotenv.config()

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

let swagger_options = {}
if (process.env.ENABLE_AUTHENTICATION === 'true') {
Expand All @@ -41,7 +41,6 @@ class App {
}

private middleware() {
this.express.use(express.json({ limit: '50mb' }))
this.express.use(express.urlencoded({ extended: false }))
this.express.use(Helmet())
this.express.use(cors({
Expand All @@ -58,7 +57,7 @@ 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
// Authentication functions/methods
this.express.use(Authentication.wrapperHandleAuthRoutes)
this.express.use(Authentication.withLogtoWrapper)
this.express.use(Authentication.guard)
Expand All @@ -78,16 +77,19 @@ class App {

private routes() {
const app = this.express
const jsonMiddleware = express.json({ limit: '50mb' })
const rawMiddleware = express.raw({ type: '*/*' })
// routes
app.get('/', (req, res) => res.redirect('swagger'))

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

// credentials
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)
app.post('/credential/suspend', new CredentialController().suspend)
app.post('/credential/reinstate', new CredentialController().reinstate)
app.post(`/credential/issue`, jsonMiddleware, CredentialController.issueValidator, new CredentialController().issue)
app.post(`/credential/verify`, jsonMiddleware, CredentialController.credentialValidator, new CredentialController().verify)
app.post(`/credential/revoke`, jsonMiddleware, CredentialController.credentialValidator, new CredentialController().revoke)
app.post('/credential/suspend', jsonMiddleware, new CredentialController().suspend)
app.post('/credential/reinstate', jsonMiddleware, new CredentialController().reinstate)

//credential-status
app.post('/credential-status/statusList2021/create', RevocationController.didValidator, RevocationController.statusListValidator, new RevocationController().createStatusList)
Expand All @@ -107,6 +109,9 @@ class App {
// customer
app.post(`/account`, new CustomerController().create)
app.get(`/account`, new CustomerController().get)
// LogTo webhooks
app.post(`/account/set-default-role`, rawMiddleware, LogToWebHook.verifyHookSignature, new CustomerController().setupDefaultRole)


// static files
app.get('/static/custom-button.js',
Expand Down
18 changes: 18 additions & 0 deletions 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 && resp.status !== 200) {
return response.status(resp.status).json({
error: resp.error})
}
return response.status(200).json({})
}
}
return response.status(400).json({})
}
}
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 @@ -65,6 +66,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
12 changes: 7 additions & 5 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 @@ -96,6 +94,10 @@ export abstract class AbstractAuthHandler implements IAuthResourceHandler
}
}

public async setup() {
await this.logToHelper.setup()
}

// interface implementation
public setNext(handler: IAuthResourceHandler): IAuthResourceHandler {
this.nextHandler = handler;
Expand Down
136 changes: 132 additions & 4 deletions src/middleware/auth/logto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export class LogToHelper {
this.allScopes = []
this.defaultScopes = []
this.allResourceWithNames = []
this.setup()
}

public async setup(): Promise<ILogToErrorResponse | void>{
Expand Down Expand Up @@ -50,6 +49,135 @@ export class LogToHelper {
return this.allResourceWithNames
}

public async setDefaultRoleForUser(userId: string): Promise<ILogToErrorResponse | void> {
const roles = await this.getRolesForUser(userId)
if (roles && roles.status === 200 && roles.data.length === 0) {
const default_role = await this.getDefaultRole()
if (default_role && default_role.status === 200) {
return await this.assignDefaultRoleForUser(userId, default_role.data.id)
}
}
}

private async assignDefaultRoleForUser(userId: string, roleId: string): Promise<ILogToErrorResponse | void> {
const uri = new URL(`/api/roles/${roleId}/users`, process.env.LOGTO_ENDPOINT);
try {
const body = {
userIds: [userId],
};
const response = await fetch(uri, {
headers: {
Authorization: 'Bearer ' + this.m2mToken,
'Content-Type': 'application/json',
},
method: 'POST',
body: JSON.stringify(body),
});

if (response.ok) {

return {
status: 200,
error: "",
data: {}
}
}
return {
error: await response.text(),
status: response.status,
data: {}
}


} catch (err) {
return {
error: `getRolesForUser ${err}`,
status: 500,
data: {}
}
}
}

private async getDefaultRole(): Promise<ILogToErrorResponse | void> {
const uri = new URL(`/api/roles`, process.env.LOGTO_ENDPOINT);
try {
const response = await fetch(uri, {
headers: {
Authorization: 'Bearer ' + this.m2mToken,
},
});

if (response.ok) {

const metadata = await response.json()

for (const role of metadata) {
if (role.name === process.env.LOGTO_DEFAULT_ROLE) {
return {
status: 200,
error: "",
data: role
}
}
}

return {
status: 200,
error: "",
data: metadata
}
}
return {
error: await response.text(),
status: response.status,
data: {}
}


} catch (err) {
return {
error: `getRolesForUser ${err}`,
status: 500,
data: {}
}
}
}

private async getRolesForUser(userId: string): Promise<ILogToErrorResponse | void> {
const uri = new URL(`/api/users/${userId}/roles`, process.env.LOGTO_ENDPOINT);
try {
const response = await fetch(uri, {
headers: {
Authorization: 'Bearer ' + this.m2mToken,
},
});

if (response.ok) {

const metadata = await response.json()

return {
status: 200,
error: "",
data: metadata
}
}
return {
error: await response.text(),
status: response.status,
data: {}
}


} catch (err) {
return {
error: `getRolesForUser ${err}`,
status: 500,
data: {}
}
}
}

private async setDefaultScopes(): Promise<ILogToErrorResponse | void>{
const _r = await this.getAllResources()
if (_r.status === 200) {
Expand Down Expand Up @@ -89,7 +217,7 @@ export class LogToHelper {
},
});
const data = await response.json();
if (response.status === 200) {
if (response.ok) {
this.m2mToken = data.access_token
}
} catch (err) {
Expand Down Expand Up @@ -141,7 +269,7 @@ export class LogToHelper {
},
});

if (response.status === 200) {
if (response.ok) {

const metadata = await response.json()
for (const sc of metadata) {
Expand Down Expand Up @@ -179,7 +307,7 @@ export class LogToHelper {
},
});

if (response.status === 200) {
if (response.ok) {
const metadata = (await response.json())
return {
error: "",
Expand Down
Loading

0 comments on commit b1da7a4

Please sign in to comment.