From 72b04b421bec0233085e34dba8e958909f1e059a Mon Sep 17 00:00:00 2001 From: Alexey Lunin <alexey.lunin@vereign.com> Date: Mon, 5 Feb 2024 08:08:45 +0000 Subject: [PATCH] feat: login with vc --- agent-swagger.json | 11 ++ .../ci-cd/helm/templates/deployment.yaml | 20 +++- apps/agent/deployment/ci-cd/helm/values.yaml | 19 ++- apps/agent/src/app/app.module.ts | 2 + apps/agent/src/main.ts | 2 + .../deployment/ci-cd/helm/values.yaml | 4 +- apps/dashboard/src/components/App/index.tsx | 12 +- .../src/components/RequireAuth/index.tsx | 15 ++- .../src/components/Sidebar/index.tsx | 10 +- .../src/components/VaultHeader/index.tsx | 5 - .../components/VaultHeader/styles.module.scss | 6 - apps/dashboard/src/hooks/auth/context.ts | 4 +- apps/dashboard/src/hooks/auth/useAuth.ts | 40 +++++-- apps/dashboard/src/hooks/withAuthorize.ts | 31 ----- .../src/modals/NewSchemaDialog/index.tsx | 7 +- .../src/modals/RequestProofDialog/index.tsx | 12 +- apps/dashboard/src/routes/config.ts | 8 +- apps/dashboard/src/routes/index.tsx | 21 ++-- .../pages/LoginPage/AuthForm/AuthFormStore.ts | 19 ++- .../routes/pages/LoginPage/AuthForm/index.tsx | 52 ++++++--- .../LoginPage/AuthForm/styles.module.scss | 7 ++ apps/dashboard/src/utils/displayError.tsx | 12 +- libs/askar/src/agent.utils.ts | 81 ++++++------- .../askar/src/askar-rest/askar.rest.module.ts | 36 ++---- libs/askar/src/askar-rest/auth/auth.guard.ts | 83 ++++++++++++++ libs/askar/src/askar-rest/auth/basic.guard.ts | 9 -- .../src/askar-rest/auth/basic.middleware.ts | 48 -------- .../src/askar-rest/auth/basic.strategy.ts | 30 ----- libs/askar/src/askar-rest/rest.controller.ts | 3 + libs/askar/src/askar/agent.service.ts | 2 +- libs/askar/src/askar/askar.service.ts | 12 +- libs/clients/src/ocmengine-client.ts | 108 ++++++++++++------ libs/config/src/config/agent.config.ts | 9 +- libs/config/src/index.ts | 1 + .../src/interfaces/agent.config.interface.ts | 8 +- libs/config/src/schemas/agent.schema.ts | 2 - libs/config/src/schemas/auth.schema.ts | 7 ++ libs/nats/src/consumer.nats.service.ts | 2 +- package.json | 1 + yarn.lock | 87 +++++++++++++- 40 files changed, 528 insertions(+), 320 deletions(-) delete mode 100644 apps/dashboard/src/hooks/withAuthorize.ts create mode 100644 libs/askar/src/askar-rest/auth/auth.guard.ts delete mode 100644 libs/askar/src/askar-rest/auth/basic.guard.ts delete mode 100644 libs/askar/src/askar-rest/auth/basic.middleware.ts delete mode 100644 libs/askar/src/askar-rest/auth/basic.strategy.ts create mode 100644 libs/config/src/schemas/auth.schema.ts diff --git a/agent-swagger.json b/agent-swagger.json index 29bddefd..5abd221f 100644 --- a/agent-swagger.json +++ b/agent-swagger.json @@ -990,6 +990,17 @@ "tags": [], "servers": [], "components": { + "securitySchemes": { + "bearer": { + "scheme": "bearer", + "bearerFormat": "JWT", + "type": "http" + }, + "basic": { + "type": "http", + "scheme": "basic" + } + }, "schemas": { "CreateInvitationResponseDto": { "type": "object", diff --git a/apps/agent/deployment/ci-cd/helm/templates/deployment.yaml b/apps/agent/deployment/ci-cd/helm/templates/deployment.yaml index d23d200c..fb4c5daf 100644 --- a/apps/agent/deployment/ci-cd/helm/templates/deployment.yaml +++ b/apps/agent/deployment/ci-cd/helm/templates/deployment.yaml @@ -69,6 +69,12 @@ spec: value: {{ .Values.ocm.agent.rateLimit | quote }} - name: ALLOWED_ORIGINS value: {{ .Values.ocm.agent.allowedOrigin | quote }} + - name: AUTH_BASIC_USER + value: {{ .Values.ocm.agent.api.basic.user | quote }} + - name: AUTH_BASIC_PASS + value: {{ .Values.ocm.agent.api.basic.pass | quote }} + - name: AUTH_JWT_PUBLIC_KEY + value: {{ .Values.ocm.agent.api.jwt.publicKey | quote }} {{- else if eq .Release.Namespace "ocm-test" }} - name: LEDGERS value: {{ .Values.ocmtest.agent.ledgers | quote }} @@ -102,6 +108,12 @@ spec: value: {{ .Values.ocmtest.agent.rateLimit | quote }} - name: ALLOWED_ORIGINS value: {{ .Values.ocmtest.agent.allowedOrigin | quote }} + - name: AUTH_BASIC_USER + value: {{ .Values.ocmtest.agent.api.basic.user | quote }} + - name: AUTH_BASIC_PASS + value: {{ .Values.ocmtest.agent.api.basic.pass | quote }} + - name: AUTH_JWT_PUBLIC_KEY + value: {{ .Values.ocmtest.agent.api.jwt.publicKey | quote }} {{- else if eq .Release.Namespace "hin" }} - name: LEDGERS value: {{ .Values.hin.agent.ledgers | quote }} @@ -138,7 +150,13 @@ spec: - name: ALLOWED_ORIGINS value: {{ .Values.hin.agent.allowedOrigin | quote }} - name: AGENT_OOB_GOALS - value: {{ .Values.hin.agent.invitationGoals| quote }} + value: {{ .Values.hin.agent.invitationGoals | quote }} + - name: AUTH_BASIC_USER + value: {{ .Values.hin.agent.api.basic.user | quote }} + - name: AUTH_BASIC_PASS + value: {{ .Values.hin.agent.api.basic.pass | quote }} + - name: AUTH_JWT_PUBLIC_KEY + value: {{ .Values.hin.agent.api.jwt.publicKey | quote }} {{- end }} {{- if .Values.extraVars }} {{ toYaml .Values.extraVars | indent 8 }} diff --git a/apps/agent/deployment/ci-cd/helm/values.yaml b/apps/agent/deployment/ci-cd/helm/values.yaml index 1144237a..9bc4c9fe 100644 --- a/apps/agent/deployment/ci-cd/helm/values.yaml +++ b/apps/agent/deployment/ci-cd/helm/values.yaml @@ -81,6 +81,12 @@ ocm: maxMessage: 10 rateLimit: 5 allowedOrigin: "*" + api: + jwt: + publicKey: "" + basic: + user: "ocm-admin" + pass: "" # nats: # server: "nats.ocm:4222" @@ -120,6 +126,12 @@ ocmtest: maxMessage: 10 rateLimit: 5 allowedOrigin: "*" + api: + jwt: + publicKey: "" + basic: + user: "ocmtest-admin" + pass: "" # nats: @@ -165,7 +177,12 @@ hin: pass: "" allowedOrigin: "*" invitationGoals: "connection.exchange" - + api: + jwt: + publicKey: "" + basic: + user: "ocmhin-admin" + pass: "" service: port: 8080 diff --git a/apps/agent/src/app/app.module.ts b/apps/agent/src/app/app.module.ts index 151009b8..203fd082 100644 --- a/apps/agent/src/app/app.module.ts +++ b/apps/agent/src/app/app.module.ts @@ -5,6 +5,7 @@ import { ConfigModule } from "@nestjs/config"; import { agentConfig, agentSchema, + authSchema, gatewayConfig, gatewaySchema, ledgersConfig, @@ -16,6 +17,7 @@ import Joi from "joi"; const validationSchema = Joi.object({ agent: agentSchema, + auth: authSchema, ledgers: ledgersSchema, nats: natsSchema, gateway: gatewaySchema, diff --git a/apps/agent/src/main.ts b/apps/agent/src/main.ts index 426c955e..0e76b0d0 100644 --- a/apps/agent/src/main.ts +++ b/apps/agent/src/main.ts @@ -32,6 +32,8 @@ async function bootstrap() { .setTitle("Agent") .setDescription("Agent API") .setVersion("1.0") + .addBearerAuth() + .addBasicAuth() .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/apps/dashboard/deployment/ci-cd/helm/values.yaml b/apps/dashboard/deployment/ci-cd/helm/values.yaml index 9d19349e..71840be4 100644 --- a/apps/dashboard/deployment/ci-cd/helm/values.yaml +++ b/apps/dashboard/deployment/ci-cd/helm/values.yaml @@ -62,7 +62,7 @@ ocm: agent: ws: "wss://ssi-dev.vereign.com/api-issuer" http: "https://ssi-dev.vereign.com/api-issuer" - tsa: "https://tsa.vereign.com" + tsa: "https://ssi-dev.vereign.com/ocm/login" basepath: "/ocm/dashboard" ocmtest: @@ -70,7 +70,7 @@ ocmtest: agent: ws: "wss://ssi-dev.vereign.com/api-holder" http: "https://ssi-dev.vereign.com/api-holder" - tsa: "https://tsa.vereign.com" + tsa: "https://ssi-dev.vereign.com/ocm-test/login" basepath: "/ocm-test/dashboard" service: diff --git a/apps/dashboard/src/components/App/index.tsx b/apps/dashboard/src/components/App/index.tsx index 902d5a12..4050dbcd 100644 --- a/apps/dashboard/src/components/App/index.tsx +++ b/apps/dashboard/src/components/App/index.tsx @@ -3,23 +3,27 @@ import { ToastContainer } from "react-toastify"; import PageRoutes from "@dashboard/routes"; import modalStore from "@dashboard/store/modalStore"; import { observer } from "mobx-react"; -import s from "./styles.module.scss"; import useGlobalAuth from "@dashboard/hooks/useGlobalAuth"; import { setConfig as setOcmEngineConfig } from "@dashboard/engine-api"; import getConfig from "@dashboard/utils/getConfig"; +import s from "./styles.module.scss"; const App = observer(() => { const auth = useGlobalAuth(); - const [isAppLoading] = useState(false); + const [isAppLoading, setAppLoading] = useState(true); useEffect(() => { const config = getConfig(); setOcmEngineConfig({ + onUnauthorized: () => { + auth.setToken(null); + }, wsUrl: config.OCMENGINE_WS_URL, httpUrl: config.OCMENGINE_HTTP_URL, - getToken: async () => auth.token || "", + getToken: async () => auth.getToken() || "", }); - }, [auth.token]); + setAppLoading(false); + }, [auth]); let content; if (isAppLoading) { diff --git a/apps/dashboard/src/components/RequireAuth/index.tsx b/apps/dashboard/src/components/RequireAuth/index.tsx index f9911f3f..0690a2d8 100644 --- a/apps/dashboard/src/components/RequireAuth/index.tsx +++ b/apps/dashboard/src/components/RequireAuth/index.tsx @@ -7,9 +7,10 @@ import { Spin } from "antd"; export interface RequireAuthProps { children: React.JSX.Element; + restricted: boolean; } -const RequireAuth = observer(({ children }: RequireAuthProps) => { +const RequireAuth = observer(({ children, restricted }: RequireAuthProps) => { const { loading, authorized } = useGlobalAuth(); const location = useLocation(); @@ -17,11 +18,11 @@ const RequireAuth = observer(({ children }: RequireAuthProps) => { return <Spin />; } - if (!authorized) { - // Redirect them to the /login page, but save the current location they were - // trying to go to when they were redirected. This allows us to send them - // along to that page after they login, which is a nicer user experience - // than dropping them off on the home page. + if ((authorized && restricted) || (!authorized && !restricted)) { + return children; + } else if (authorized && !restricted) { + return <Navigate to={config.connection_list.getLink()} replace />; + } else { return ( <Navigate to={config.login.getLink()} @@ -30,8 +31,6 @@ const RequireAuth = observer(({ children }: RequireAuthProps) => { /> ); } - - return children; }); export default RequireAuth; diff --git a/apps/dashboard/src/components/Sidebar/index.tsx b/apps/dashboard/src/components/Sidebar/index.tsx index 3ccd9cf6..2b6c4b2f 100644 --- a/apps/dashboard/src/components/Sidebar/index.tsx +++ b/apps/dashboard/src/components/Sidebar/index.tsx @@ -1,9 +1,11 @@ import React from "react"; import { Link } from "react-router-dom"; import config from "@dashboard/routes/config"; +import useGlobalAuth from "@dashboard/hooks/useGlobalAuth"; import s from "./styles.module.scss"; const Sidebar = () => { + const auth = useGlobalAuth(); return ( <div className={s.sidebar}> <div className={s.sidebarBar} /> @@ -34,7 +36,13 @@ const Sidebar = () => { VCM self-issued credentials </Link> <div className={s.space} /> - <Link className={s.logoutLink} to={config.welcome.getLink()}> + <Link + onClick={() => { + auth.setToken(null); + }} + className={s.logoutLink} + to={config.welcome.getLink()} + > Logout </Link> </div> diff --git a/apps/dashboard/src/components/VaultHeader/index.tsx b/apps/dashboard/src/components/VaultHeader/index.tsx index 27d829d3..d40456f9 100644 --- a/apps/dashboard/src/components/VaultHeader/index.tsx +++ b/apps/dashboard/src/components/VaultHeader/index.tsx @@ -31,11 +31,6 @@ const VaultHeader: React.FC<Props> = ({ hideTry }) => { )} </div> <div className={s.column}> - <div className={s.toDashboard}> - <Link to={config.connection_list.getLink()}> - Jump to dashboard - </Link> - </div> <div> <img className={s.vaultImage} diff --git a/apps/dashboard/src/components/VaultHeader/styles.module.scss b/apps/dashboard/src/components/VaultHeader/styles.module.scss index d60adbf7..377f11ce 100644 --- a/apps/dashboard/src/components/VaultHeader/styles.module.scss +++ b/apps/dashboard/src/components/VaultHeader/styles.module.scss @@ -76,9 +76,3 @@ color: #14b7cc; font-weight: bold; } - -.toDashboard { - position: absolute; - top: 40px; - right: 150px; -} diff --git a/apps/dashboard/src/hooks/auth/context.ts b/apps/dashboard/src/hooks/auth/context.ts index 2bb8a4d1..7a8eeef3 100644 --- a/apps/dashboard/src/hooks/auth/context.ts +++ b/apps/dashboard/src/hooks/auth/context.ts @@ -3,7 +3,7 @@ import { createContext } from "react"; interface Auth { authorized: boolean; loading: boolean; - token: string | null; + getToken: () => string | null; setToken: (token: string | null) => void; } @@ -13,7 +13,7 @@ const authContext = createContext<Auth>({ setToken: () => { console.warn("Context not overwritten"); }, - token: null, + getToken: () => null, }); export default authContext; diff --git a/apps/dashboard/src/hooks/auth/useAuth.ts b/apps/dashboard/src/hooks/auth/useAuth.ts index 82e205e3..2eda3980 100644 --- a/apps/dashboard/src/hooks/auth/useAuth.ts +++ b/apps/dashboard/src/hooks/auth/useAuth.ts @@ -2,16 +2,41 @@ import { useEffect, useState } from "react"; const LS_KEY = "token"; +function isJwtValid(token: string) { + try { + const parts = token.split("."); + if (parts.length !== 3) { + throw new Error("Invalid JWT token"); + } + + const payload = JSON.parse(atob(parts[1])); + + if (!payload.exp) { + throw new Error("JWT token does not contain expiration time"); + } + + const currentTime = Math.floor(Date.now() / 1000); + return payload.exp > currentTime; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(e.message); + } + return false; + } +} + const useAuth = () => { const [authorized, setAuthorized] = useState(false); - const [token, setToken] = useState<string | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { const lsToken = localStorage.getItem(LS_KEY); if (lsToken) { - setToken(lsToken); - setAuthorized(true); + if (isJwtValid(lsToken)) { + setAuthorized(true); + } else { + localStorage.removeItem(LS_KEY); + } } setLoading(false); }, []); @@ -19,20 +44,17 @@ const useAuth = () => { const updateToken = (token: string | null) => { if (token) { localStorage.setItem(LS_KEY, token); - setToken(token); setAuthorized(true); } else { localStorage.removeItem(LS_KEY); - setToken(null); setAuthorized(false); } }; return { - token, - // TODO disable login functionality - authorized: true, - loading: false, + authorized, + loading, + getToken: () => localStorage.getItem(LS_KEY), setToken: updateToken, }; }; diff --git a/apps/dashboard/src/hooks/withAuthorize.ts b/apps/dashboard/src/hooks/withAuthorize.ts deleted file mode 100644 index b6b018d4..00000000 --- a/apps/dashboard/src/hooks/withAuthorize.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect, useState } from "react"; - -const LS_KEY = "token"; - -const useAuth = () => { - const [authorized, setAuthorized] = useState(false); - const [loading, setLoading] = useState(true); - - useEffect(() => { - localStorage.getItem(LS_KEY) && setAuthorized(true); - setLoading(false); - }, []); - - const setToken = (token: string | null) => { - if (token) { - localStorage.setItem(LS_KEY, token); - setAuthorized(true); - } else { - localStorage.removeItem(LS_KEY); - setAuthorized(false); - } - }; - - return { - authorized, - loading, - setToken, - }; -}; - -export default useAuth; diff --git a/apps/dashboard/src/modals/NewSchemaDialog/index.tsx b/apps/dashboard/src/modals/NewSchemaDialog/index.tsx index c31e905e..40e3c035 100644 --- a/apps/dashboard/src/modals/NewSchemaDialog/index.tsx +++ b/apps/dashboard/src/modals/NewSchemaDialog/index.tsx @@ -62,12 +62,11 @@ const NewSchemaDialog = observer( { required: true, message: "Please enter version!" }, { pattern: /^(\d+\.)?(\d+\.)?(\*|\d+)$/, - message: 'Invalid format. Please enter the version number in the format \'major.minor.patch\', e.g., \'1.0.0\', \'1.0\' or \'1\'.', - } + message: + "Invalid format. Please enter the version number in the format 'major.minor.patch', e.g., '1.0.0', '1.0' or '1'.", + }, ]} > - - <Input /> </Form.Item> <Form.Item diff --git a/apps/dashboard/src/modals/RequestProofDialog/index.tsx b/apps/dashboard/src/modals/RequestProofDialog/index.tsx index 98426a3d..09b3d483 100644 --- a/apps/dashboard/src/modals/RequestProofDialog/index.tsx +++ b/apps/dashboard/src/modals/RequestProofDialog/index.tsx @@ -36,9 +36,7 @@ const RequestProofDialog = observer( <span>{rowIndex + 1}.</span> </div> <div className={s.field}> - <div className={s.field__label}> - Credential definition - </div> + <div className={s.field__label}>Credential definition</div> <Select placeholder="Select credential" // style={{ width: 300 }} @@ -51,12 +49,10 @@ const RequestProofDialog = observer( }))} /> </div> - {row.schemaLoading && <Spin/>} + {row.schemaLoading && <Spin />} {row.schema && ( <div className={s.field}> - <div className={s.field__label}> - Attribute - </div> + <div className={s.field__label}>Attribute</div> <Select placeholder="Select attribute" // style={{ width: 300 }} @@ -80,7 +76,7 @@ const RequestProofDialog = observer( </Button> </div> </Space> - ))} + ))} <Space direction="vertical" className={s.actions}> <Button onClick={() => store.addRow()}>Add row</Button> <Button diff --git a/apps/dashboard/src/routes/config.ts b/apps/dashboard/src/routes/config.ts index 7c58f039..f5f42ebe 100644 --- a/apps/dashboard/src/routes/config.ts +++ b/apps/dashboard/src/routes/config.ts @@ -66,13 +66,13 @@ const routes = { Component: CreatedDidListPage, route: "/created-dids", getLink: () => "/created-dids", - requireAuth: false, + requireAuth: true, }, createdInvitations: { Component: CreatedInvitationListPage, route: "/created-invitations", getLink: () => "/created-invitations", - requireAuth: false, + requireAuth: true, }, login: { Component: LoginPage, @@ -84,13 +84,13 @@ const routes = { Component: ResolveDidPage, route: "/resolve-did", getLink: () => "/resolve-did", - requireAuth: false, + requireAuth: true, }, vcmSelfIssue: { Component: VcmSelfIssuePage, route: "/self-issued-creds-in-vcm", getLink: () => "/self-issued-creds-in-vcm", - requireAuth: false, + requireAuth: true, }, authToken: { Component: RedirectWithTokenPage, diff --git a/apps/dashboard/src/routes/index.tsx b/apps/dashboard/src/routes/index.tsx index 369a730a..5ee65a6b 100644 --- a/apps/dashboard/src/routes/index.tsx +++ b/apps/dashboard/src/routes/index.tsx @@ -6,20 +6,17 @@ import config from "./config"; function PageRoutes() { return ( <Routes> - {Object.values(config).map((item) => { - let component; - if (item.requireAuth) { - component = ( - <RequireAuth> + {Object.values(config).map((item) => ( + <Route + key={item.route} + path={item.route} + element={ + <RequireAuth restricted={item.requireAuth}> <item.Component /> </RequireAuth> - ); - } else { - component = <item.Component />; - } - - return <Route key={item.route} path={item.route} element={component} />; - })} + } + /> + ))} </Routes> ); } diff --git a/apps/dashboard/src/routes/pages/LoginPage/AuthForm/AuthFormStore.ts b/apps/dashboard/src/routes/pages/LoginPage/AuthForm/AuthFormStore.ts index 216ca223..df563de9 100644 --- a/apps/dashboard/src/routes/pages/LoginPage/AuthForm/AuthFormStore.ts +++ b/apps/dashboard/src/routes/pages/LoginPage/AuthForm/AuthFormStore.ts @@ -2,6 +2,7 @@ import { makeAutoObservable, runInAction } from "mobx"; import tsaApi from "@dashboard/tsa-api"; import { toast } from "react-toastify"; import displayError from "@dashboard/utils/displayError"; +import { ApiException } from "@dashboard/engine-api"; type SetToken = (token: string) => void; @@ -36,7 +37,11 @@ class AuthFormStore { }; public requestTsaProof = async () => { - runInAction(() => (this.loading = true)); + runInAction(() => { + this.loading = true; + this.acceptanceAwaiting = false; + this.acceptanceExpired = false; + }); try { const { proofRecordId, proofUrl, proofUrlShort } = await tsaApi.loginLoginInvitation(); @@ -66,7 +71,17 @@ class AuthFormStore { this.acceptanceAwaiting = false; }); } catch (e: unknown) { - displayError(e); + if ( + e instanceof ApiException || + (e as { isApiException: boolean }).isApiException + ) { + const apiExError = e as ApiException; + if (apiExError.status !== 504) { + displayError(e); + } + } else { + displayError(e); + } runInAction(() => { this.acceptanceAwaiting = false; this.acceptanceExpired = true; diff --git a/apps/dashboard/src/routes/pages/LoginPage/AuthForm/index.tsx b/apps/dashboard/src/routes/pages/LoginPage/AuthForm/index.tsx index ce060719..1c970046 100644 --- a/apps/dashboard/src/routes/pages/LoginPage/AuthForm/index.tsx +++ b/apps/dashboard/src/routes/pages/LoginPage/AuthForm/index.tsx @@ -2,16 +2,15 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import AuthFormStore from "./AuthFormStore"; import { Button, Form, Input, QRCode } from "antd"; -import s from "./styles.module.scss"; import useGlobalAuth from "@dashboard/hooks/useGlobalAuth"; +import s from "./styles.module.scss"; const LoginPage = observer(() => { const auth = useGlobalAuth(); const [store] = useState(() => new AuthFormStore(auth.setToken)); const handleEnterEmail = ({ email }: { email: string }) => { - // TODO Functional deactivated. Auth will be implemented in another branch - // store.requestTsaAuthEmail(email); + store.requestTsaAuthEmail(email); }; return ( @@ -53,10 +52,10 @@ const LoginPage = observer(() => { <br /> <Button + type="link" onClick={() => { store.mode = "vc"; - // Functional deactivated - // store.requestTsaProof(); + store.requestTsaProof(); }} > Login using VC @@ -69,21 +68,34 @@ const LoginPage = observer(() => { <div className={s.qrWrapper}> <QRCode status={ - !store.proofUrlShort || store.loading ? "loading" : "active" + store.acceptanceExpired + ? "expired" + : !store.proofUrlShort || store.loading + ? "loading" + : "active" } value={store.proofUrlShort || "loading"} size={400} - icon={`${window.BASE_PATH}/assets/Vereign_Logo_ICON_BLACK.png}`} + icon={getFullHttpPath("/assets/Vereign_Logo_ICON_BLACK.png")} /> </div> - <div> - In case you have another OCM with VC. You can copy this url and - accept it - <br /> - <div>{store.proofUrl}</div> - </div> + {store.acceptanceExpired && ( + <> + <br /> + <Button + type="link" + onClick={() => { + store.requestTsaProof(); + }} + > + Refresh + </Button> + </> + )} + <br /> <Button + type="link" onClick={() => { store.mode = "email"; }} @@ -96,4 +108,18 @@ const LoginPage = observer(() => { ); }); +function getFullHttpPath(partialPath: string) { + const domain = window.location.origin; + const BASE_PATH = window.BASE_PATH; + + const formattedBasePath = BASE_PATH.endsWith("/") + ? BASE_PATH + : `${BASE_PATH}/`; + const formattedPartialPath = partialPath.startsWith("/") + ? partialPath.substring(1) + : partialPath; + + return `${domain}${formattedBasePath}${formattedPartialPath}`; +} + export default LoginPage; diff --git a/apps/dashboard/src/routes/pages/LoginPage/AuthForm/styles.module.scss b/apps/dashboard/src/routes/pages/LoginPage/AuthForm/styles.module.scss index a04fe6e0..a147f487 100644 --- a/apps/dashboard/src/routes/pages/LoginPage/AuthForm/styles.module.scss +++ b/apps/dashboard/src/routes/pages/LoginPage/AuthForm/styles.module.scss @@ -10,6 +10,7 @@ border: 3px solid transparent; display: flex; flex-direction: column; + position: relative; } .formTitle { text-align: center; @@ -30,3 +31,9 @@ margin: 0 0 100px 0; text-align: center; } + +.adminLogin { + position: absolute; + right: 0; + top: 0; +} diff --git a/apps/dashboard/src/utils/displayError.tsx b/apps/dashboard/src/utils/displayError.tsx index 7f1e87d1..89e89e4a 100644 --- a/apps/dashboard/src/utils/displayError.tsx +++ b/apps/dashboard/src/utils/displayError.tsx @@ -25,23 +25,27 @@ function combine(msg1: undefined | string, msg2: string | string[]) { const displayError = (e: unknown, msg?: string) => { console.error(e); - if (e instanceof ApiException) { + if ( + e instanceof ApiException || + (e as { isApiException: boolean }).isApiException + ) { + const apiExError = e as ApiException; let serverResponse: { message: string | string[]; statusCode: number; } | null; try { - serverResponse = JSON.parse(e.response); + serverResponse = JSON.parse(apiExError.response); } catch (parseError: unknown) { console.error("Can not parse the server response"); - toast.error(combine(msg, e.message)); + toast.error(combine(msg, apiExError.message)); return; } if (serverResponse && serverResponse.message) { toast.error(combine(msg, serverResponse.message)); } else { - toast.error(combine(msg, e.message)); + toast.error(combine(msg, apiExError.message)); } return; } diff --git a/libs/askar/src/agent.utils.ts b/libs/askar/src/agent.utils.ts index ca9c060d..4ec26178 100644 --- a/libs/askar/src/agent.utils.ts +++ b/libs/askar/src/agent.utils.ts @@ -129,7 +129,7 @@ export const generateKey = async ({ export const generateDidWeb = async ({ seed, agent, - peerAddress + peerAddress, }: { seed: string; agent: Agent; @@ -141,7 +141,7 @@ export const generateDidWeb = async ({ const parsedUrl = url.parse(peerAddress); let hostname = parsedUrl.hostname!; const port = parsedUrl.port; - let pathname = parsedUrl.pathname?.replace(/^\/+|\/+$/g, ''); + const pathname = parsedUrl.pathname?.replace(/^\/+|\/+$/g, ""); // If port is specified, encode it if (port) { @@ -150,7 +150,7 @@ export const generateDidWeb = async ({ // Convert URLs to 'did:web' form let didWeb = `did:web:${hostname}`; if (pathname) { - didWeb += `:${pathname.replace(/\//g, ':')}`; + didWeb += `:${pathname.replace(/\//g, ":")}`; } const verificationMethodKey0Id = `${didWeb}#jwt-key0`; @@ -158,29 +158,23 @@ export const generateDidWeb = async ({ const jsonDidDoc = { "@context": [ "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/suites/ed25519-2018/v1" + "https://w3id.org/security/suites/ed25519-2018/v1", ], - "id": didWeb, - "verificationMethod": [ + id: didWeb, + verificationMethod: [ { - "id": verificationMethodKey0Id, - "type": "Ed25519VerificationKey2018", - "controller": didWeb, - "publicKeyBase58": pubKey.publicKeyBase58 + id: verificationMethodKey0Id, + type: "Ed25519VerificationKey2018", + controller: didWeb, + publicKeyBase58: pubKey.publicKeyBase58, }, ], - "authentication": [ - verificationMethodKey0Id - ], - "assertionMethod": [ - verificationMethodKey0Id - ], - "keyAgreement": [ - verificationMethodKey0Id - ] + authentication: [verificationMethodKey0Id], + assertionMethod: [verificationMethodKey0Id], + keyAgreement: [verificationMethodKey0Id], }; - const didDocumentInstance = JsonTransformer.fromJSON(jsonDidDoc, DidDocument) + const didDocumentInstance = JsonTransformer.fromJSON(jsonDidDoc, DidDocument); const recordId = "did:web"; const existingRecord = await agent.genericRecords.findById(recordId); @@ -189,13 +183,13 @@ export const generateDidWeb = async ({ } await agent.genericRecords.save({ id: recordId, - content: jsonDidDoc + content: jsonDidDoc, }); await agent.dids.import({ did: didWeb, didDocument: didDocumentInstance, - overwrite: false + overwrite: false, }); console.log("Generated did:web"); @@ -254,7 +248,7 @@ export const getAskarAnonCredsIndyModules = (networks: any) => { new KeyDidResolver(), new PeerDidResolver(), new WebDidResolver(), - new JwkDidResolver() + new JwkDidResolver(), ], }), askar: new AskarModule({ @@ -571,9 +565,13 @@ export const attachShortUrlHandler = (server: Express, agent: Agent): void => { ); }; -export const attachDidWebHandler = (server: Express, agent: Agent, agentPeerAddress: string): void => { +export const attachDidWebHandler = ( + server: Express, + agent: Agent, + agentPeerAddress: string, +): void => { const parsedUrl = url.parse(agentPeerAddress); - const pathname = parsedUrl.pathname?.replace(/^\/+|\/+$/g, ''); + const pathname = parsedUrl.pathname?.replace(/^\/+|\/+$/g, ""); let serverDidWebPath: string; if (pathname) { @@ -582,26 +580,21 @@ export const attachDidWebHandler = (server: Express, agent: Agent, agentPeerAddr serverDidWebPath = "/.well-known/did.json"; } - console.log('Listen did web requests on path ' + serverDidWebPath); - server.get( - serverDidWebPath, - async (req: Request, res: Response) => { - try { - const didWebRecord = await agent.genericRecords.findById("did:web"); + console.log("Listen did web requests on path " + serverDidWebPath); + server.get(serverDidWebPath, async (req: Request, res: Response) => { + try { + const didWebRecord = await agent.genericRecords.findById("did:web"); - if (!didWebRecord) { - return res.status(404).send("Not found"); - } + if (!didWebRecord) { + return res.status(404).send("Not found"); + } - const didWebDoc = didWebRecord.content; + const didWebDoc = didWebRecord.content; - return res - .header("Content-Type", "application/json") - .send(didWebDoc); - } catch (error) { - console.error(error); - return res.status(500).send("Internal Server Error"); - } - }, - ); + return res.header("Content-Type", "application/json").send(didWebDoc); + } catch (error) { + console.error(error); + return res.status(500).send("Internal Server Error"); + } + }); }; diff --git a/libs/askar/src/askar-rest/askar.rest.module.ts b/libs/askar/src/askar-rest/askar.rest.module.ts index 6aff9fe7..6a320c0f 100644 --- a/libs/askar/src/askar-rest/askar.rest.module.ts +++ b/libs/askar/src/askar-rest/askar.rest.module.ts @@ -1,20 +1,21 @@ -import { - Module, - ValidationPipe, - MiddlewareConsumer, - RequestMethod, -} from "@nestjs/common"; +import { Module, ValidationPipe } from "@nestjs/common"; import { AgentService } from "../askar/agent.service"; -import { ConfigModule, ConfigService } from "@nestjs/config"; +import { ConfigModule } from "@nestjs/config"; import { LedgersModule } from "@ocm-engine/ledgers"; import { APP_PIPE } from "@nestjs/core"; import { RestController } from "./rest.controller"; import { PassportModule } from "@nestjs/passport"; -import { BasicAuthMiddleware } from "./auth/basic.middleware"; -import { IConfAgent } from "@ocm-engine/config"; +import { JwtModule } from "@nestjs/jwt"; @Module({ - imports: [ConfigModule, LedgersModule, PassportModule], + imports: [ + ConfigModule, + LedgersModule, + PassportModule, + JwtModule.register({ + global: true, + }), + ], providers: [ AgentService, { @@ -26,17 +27,4 @@ import { IConfAgent } from "@ocm-engine/config"; ], controllers: [RestController], }) -export class AskarRestModule { - constructor(private readonly configService: ConfigService) {} - - configure(consumer: MiddlewareConsumer) { - const config: IConfAgent | undefined = - this.configService.get<IConfAgent>("agent"); - - if (config?.agentIsSVDX) { - consumer - .apply(BasicAuthMiddleware) - .forRoutes({ path: "*messages", method: RequestMethod.ALL }); - } - } -} +export class AskarRestModule {} diff --git a/libs/askar/src/askar-rest/auth/auth.guard.ts b/libs/askar/src/askar-rest/auth/auth.guard.ts new file mode 100644 index 00000000..1279878a --- /dev/null +++ b/libs/askar/src/askar-rest/auth/auth.guard.ts @@ -0,0 +1,83 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + Logger, + UnauthorizedException, +} from "@nestjs/common"; +import { JwtService } from "@nestjs/jwt"; +import { Request } from "express"; +import { ConfigService } from "@nestjs/config"; +import { IConfAgent } from "@ocm-engine/config"; + +@Injectable() +export class AuthGuard implements CanActivate { + private readonly logger: Logger = new Logger(AuthGuard.name); + + constructor( + private readonly jwtService: JwtService, + private readonly configService: ConfigService, + ) {} + + async canActivate(context: ExecutionContext): Promise<boolean> { + const config: IConfAgent = this.configService.get<IConfAgent>("agent")!; + + if (!(config.agentAuthBasicEnabled || config.agentAuthJwtEnabled)) { + return true; + } + // Auth enabled + + const request = context.switchToHttp().getRequest(); + const [type, token] = this.extractAuthHeader(request); + if (!type || !token) { + throw new UnauthorizedException(); + } + + switch (type) { + case "Bearer": + if (!config.agentAuthJwtEnabled) throw new UnauthorizedException(); + await this.verifyBearerToken(token, config); + break; + case "Basic": + if (!config.agentAuthBasicEnabled) throw new UnauthorizedException(); + await this.verifyBasicToken(token, config); + break; + default: + throw new UnauthorizedException(); + } + + return true; + } + + private async verifyBearerToken(token: string, config: IConfAgent) { + try { + await this.jwtService.verifyAsync(token, { + publicKey: config.agentAuthJwtPublicKey, + }); + } catch (e) { + this.logger.log("Token verification Error"); + this.logger.log(e); + throw new UnauthorizedException(); + } + } + + private async verifyBasicToken(token: string, config: IConfAgent) { + const [username, password] = Buffer.from(token, "base64") + .toString() + .split(":"); + + if ( + username !== config.agentAuthBasicUser || + password !== config.agentAuthBasicPass + ) { + throw new UnauthorizedException(); + } + } + + private extractAuthHeader( + request: Request, + ): [string | undefined, string | undefined] { + const [type, token] = request.headers.authorization?.split(" ") ?? []; + return [type, token]; + } +} diff --git a/libs/askar/src/askar-rest/auth/basic.guard.ts b/libs/askar/src/askar-rest/auth/basic.guard.ts deleted file mode 100644 index 354ea9ed..00000000 --- a/libs/askar/src/askar-rest/auth/basic.guard.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { AuthGuard } from "@nestjs/passport"; - -/** - * Basic guard is NOT currently used, it is left for the time when we implement jwt auth for the rest of the routes - */ - -@Injectable() -export class BasicGuard extends AuthGuard("basic") {} diff --git a/libs/askar/src/askar-rest/auth/basic.middleware.ts b/libs/askar/src/askar-rest/auth/basic.middleware.ts deleted file mode 100644 index 25099e3a..00000000 --- a/libs/askar/src/askar-rest/auth/basic.middleware.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - Injectable, - Logger, - NestMiddleware, - UnauthorizedException, -} from "@nestjs/common"; -import { Request, Response, NextFunction } from "express"; -import { ConfigService } from "@nestjs/config"; -import { IConfAgent } from "@ocm-engine/config"; - -@Injectable() -export class BasicAuthMiddleware implements NestMiddleware { - private readonly logger: Logger = new Logger(BasicAuthMiddleware.name); - - constructor(private readonly configService: ConfigService) {} - - use(req: Request, res: Response, next: NextFunction) { - const config: IConfAgent | undefined = - this.configService.get<IConfAgent>("agent"); - - if (!config?.agentIsSVDX) { - this.logger.log("Agent is REST only turning of basic auth"); - - return next(); - } - - this.logger.log("Agent is SVDX turning basic auth middleware on"); - - const authHeader = req.headers["authorization"]; - - if (!authHeader) { - return next(new UnauthorizedException()); - } - - const [username, password] = Buffer.from(authHeader.split(" ")[1], "base64") - .toString() - .split(":"); - - if ( - username === config.agentSVDXBasicUser && - password === config.agentSVDXBasicPass - ) { - return next(); - } - - return next(new UnauthorizedException()); - } -} diff --git a/libs/askar/src/askar-rest/auth/basic.strategy.ts b/libs/askar/src/askar-rest/auth/basic.strategy.ts deleted file mode 100644 index f454f0af..00000000 --- a/libs/askar/src/askar-rest/auth/basic.strategy.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ConfigService } from "@nestjs/config"; -import { IConfAgent } from "@ocm-engine/config"; -import { BasicStrategy as Strategy } from "passport-http"; -import { PassportStrategy } from "@nestjs/passport"; -import { Injectable, UnauthorizedException } from "@nestjs/common"; - -/** - * Basic strategy is NOT currently used, it is left for the time when we implement jwt auth for the rest of the routes - */ -@Injectable() -export class BasicStrategy extends PassportStrategy(Strategy, "basic") { - constructor(private readonly configService: ConfigService) { - super(); - } - - async validate(username: string, password: string): Promise<boolean> { - console.log(username, password); - const config: IConfAgent | undefined = - this.configService.get<IConfAgent>("agent"); - - if ( - config?.agentSVDXBasicUser === username && - config?.agentSVDXBasicPass === password - ) { - return true; - } - - throw new UnauthorizedException(); - } -} diff --git a/libs/askar/src/askar-rest/rest.controller.ts b/libs/askar/src/askar-rest/rest.controller.ts index 29c8cf03..a39330a4 100644 --- a/libs/askar/src/askar-rest/rest.controller.ts +++ b/libs/askar/src/askar-rest/rest.controller.ts @@ -7,6 +7,7 @@ import { Param, Post, UseFilters, + UseGuards, } from "@nestjs/common"; import { AgentService } from "../askar/agent.service"; @@ -29,9 +30,11 @@ import { } from "@ocm-engine/dtos"; import { AllExceptionsHandler } from "./exception.handler"; import { DidResolutionResult } from "@aries-framework/core"; +import { AuthGuard } from "./auth/auth.guard"; @UseFilters(AllExceptionsHandler) @Controller("v1") +@UseGuards(AuthGuard) export class RestController { constructor(private readonly agentService: AgentService) {} diff --git a/libs/askar/src/askar/agent.service.ts b/libs/askar/src/askar/agent.service.ts index 464e9c4e..54b66690 100644 --- a/libs/askar/src/askar/agent.service.ts +++ b/libs/askar/src/askar/agent.service.ts @@ -1029,7 +1029,7 @@ export class AgentService { getCreatedDids = async (): Promise<DidRecordDto[]> => { const didRecords = await this.askar.agent.dids.getCreatedDids(); - return didRecords.map(p => { + return didRecords.map((p) => { const dto = new DidRecordDto(); const tags = p.getTags(); diff --git a/libs/askar/src/askar/askar.service.ts b/libs/askar/src/askar/askar.service.ts index 846af055..789ac58e 100644 --- a/libs/askar/src/askar/askar.service.ts +++ b/libs/askar/src/askar/askar.service.ts @@ -80,7 +80,11 @@ export class AskarService implements OnModuleInit, OnModuleDestroy { //handler for short url invitations, look at agent service createInvitation attachShortUrlHandler(this.server, this.agent); - attachDidWebHandler(this.server, this.agent, this.agentConfig.agentPeerAddress); + attachDidWebHandler( + this.server, + this.agent, + this.agentConfig.agentPeerAddress, + ); this.agent.registerInboundTransport( new HttpInboundTransport({ @@ -117,13 +121,15 @@ export class AskarService implements OnModuleInit, OnModuleDestroy { const didWebs = await this.agent.dids.getCreatedDids({ method: "web" }); if (didWebs.length) { for (const didsKey in didWebs) { - this.logger.debug(`agent already have ${didWebs[didsKey].did} registered`); + this.logger.debug( + `agent already have ${didWebs[didsKey].did} registered`, + ); } } else { await generateDidWeb({ agent: this.agent, seed: this.agentConfig.agentDidSeed, - peerAddress: this.agentConfig.agentPeerAddress + peerAddress: this.agentConfig.agentPeerAddress, }); } diff --git a/libs/clients/src/ocmengine-client.ts b/libs/clients/src/ocmengine-client.ts index d3ae8f16..e927c062 100644 --- a/libs/clients/src/ocmengine-client.ts +++ b/libs/clients/src/ocmengine-client.ts @@ -1,36 +1,26 @@ import { AcceptCredentialDto, AcceptProofDto, - ConnectionRecordDto, CreateCredentialDefinitionRequestDto, CreateInvitationRequestDto, - CreateInvitationResponseDto, CreateSchemaRequestDto, - CreddefRecordDto, - CredentialFormatDataDto, - CredentialOfferResponseDto, - CredentialRecordDto, IdReqDto, MakeBasicMessageRequestDto, - MessageRecordDto, OfferCredentialRequestDto, - ProofFormatDataDto, - ProofRecordDto, RequestProofDto, - RequestProofResponseDto, RestControllerClient, Role, Roles, - SchemaRecordDto, States, States2, States3, - AcceptInvitationRequestDto + AcceptInvitationRequestDto, } from "./frontend/agent_gen"; export * from "./frontend/agent_gen"; export interface Config { + onUnauthorized?: () => void; wsUrl: string; httpUrl: string; getToken: () => Promise<string>; @@ -44,39 +34,73 @@ class ApiClient { private _ws!: WebSocket; private _rest!: RestControllerClient; - public fetchInvitations = (states: States[] | undefined, roles: Roles[] | undefined) => this._rest.fetchInvitations(states, roles); - public createInvitation = (body: CreateInvitationRequestDto) => this._rest.createInvitation(body); + public fetchInvitations = ( + states: States[] | undefined, + roles: Roles[] | undefined, + ) => this._rest.fetchInvitations(states, roles); + public createInvitation = (body: CreateInvitationRequestDto) => + this._rest.createInvitation(body); public getInvitationById = (id: string) => this._rest.getInvitationById(id); - public deleteInvitationById = (id: string) => this._rest.deleteInvitationById(id); - public acceptInvitation = (body: AcceptInvitationRequestDto) => this._rest.acceptInvitation(body); + public deleteInvitationById = (id: string) => + this._rest.deleteInvitationById(id); + public acceptInvitation = (body: AcceptInvitationRequestDto) => + this._rest.acceptInvitation(body); public fetchConnections = () => this._rest.fetchConnections(); public getConnectionById = (id: string) => this._rest.getConnectionById(id); - public deleteConnectionById = (id: string) => this._rest.deleteConnectionById(id); - public getConnectionByOobId = (id: string) => this._rest.getConnectionByOobId(id); - public createSchema = (body: CreateSchemaRequestDto) => this._rest.createSchema(body); + public deleteConnectionById = (id: string) => + this._rest.deleteConnectionById(id); + public getConnectionByOobId = (id: string) => + this._rest.getConnectionByOobId(id); + public createSchema = (body: CreateSchemaRequestDto) => + this._rest.createSchema(body); public fetchSchemas = () => this._rest.fetchSchemas(); public getSchemaById = (body: IdReqDto) => this._rest.getSchemaById(body); - public fetchCredentialDefinitions = () => this._rest.fetchCredentialDefinitions(); - public createCredentialDefinition = (body: CreateCredentialDefinitionRequestDto) => this._rest.createCredentialDefinition(body); - public getCredentialDefinitionById = (body: IdReqDto) => this._rest.getCredentialDefinitionById(body); - public offerCredential = (body: OfferCredentialRequestDto) => this._rest.offerCredential(body); - public fetchCredentials = (states: States2[] | undefined, connectionId: string | undefined) => this._rest.fetchCredentials(states, connectionId); + public fetchCredentialDefinitions = () => + this._rest.fetchCredentialDefinitions(); + public createCredentialDefinition = ( + body: CreateCredentialDefinitionRequestDto, + ) => this._rest.createCredentialDefinition(body); + public getCredentialDefinitionById = (body: IdReqDto) => + this._rest.getCredentialDefinitionById(body); + public offerCredential = (body: OfferCredentialRequestDto) => + this._rest.offerCredential(body); + public fetchCredentials = ( + states: States2[] | undefined, + connectionId: string | undefined, + ) => this._rest.fetchCredentials(states, connectionId); public getCredentialById = (id: string) => this._rest.getCredentialById(id); - public deleteCredentialById = (id: string) => this._rest.deleteCredentialById(id); - public getCredentialFormatDataById = (id: string) => this._rest.getCredentialFormatDataById(id); - public acceptCredential = (body: AcceptCredentialDto) => this._rest.acceptCredential(body); - public declineCredential = (credential_record_id: string) => this._rest.declineCredential(credential_record_id); - public sendMessage = (body: MakeBasicMessageRequestDto) => this._rest.sendMessage(body); - public fetchBasicMessages = (role: Role | undefined, connectionId: string | undefined) => this._rest.fetchBasicMessages(role, connectionId); + public deleteCredentialById = (id: string) => + this._rest.deleteCredentialById(id); + public getCredentialFormatDataById = (id: string) => + this._rest.getCredentialFormatDataById(id); + public acceptCredential = (body: AcceptCredentialDto) => + this._rest.acceptCredential(body); + public declineCredential = (credential_record_id: string) => + this._rest.declineCredential(credential_record_id); + public sendMessage = (body: MakeBasicMessageRequestDto) => + this._rest.sendMessage(body); + public fetchBasicMessages = ( + role: Role | undefined, + connectionId: string | undefined, + ) => this._rest.fetchBasicMessages(role, connectionId); public deleteBasicMessage = (id: string) => this._rest.deleteBasicMessage(id); - public fetchProofs = (states: States3[] | undefined, connectionId: string | undefined) => this._rest.fetchProofs(states, connectionId); - public getProofById = (proof_record_id: string) => this._rest.getProofById(proof_record_id); - public deleteProofById = (proof_record_id: string) => this._rest.deleteProofById(proof_record_id); - public getProofFormatDataById = (proof_record_id: string) => this._rest.getProofFormatDataById(proof_record_id); - public proofAcceptanceWait = (proof_record_id: string) => this._rest.proofAcceptanceWait(proof_record_id); - public requestProof = (body: RequestProofDto) => this._rest.requestProof(body); + public fetchProofs = ( + states: States3[] | undefined, + connectionId: string | undefined, + ) => this._rest.fetchProofs(states, connectionId); + public getProofById = (proof_record_id: string) => + this._rest.getProofById(proof_record_id); + public deleteProofById = (proof_record_id: string) => + this._rest.deleteProofById(proof_record_id); + public getProofFormatDataById = (proof_record_id: string) => + this._rest.getProofFormatDataById(proof_record_id); + public proofAcceptanceWait = (proof_record_id: string) => + this._rest.proofAcceptanceWait(proof_record_id); + public requestProof = (body: RequestProofDto) => + this._rest.requestProof(body); public acceptProof = (body: AcceptProofDto) => this._rest.acceptProof(body); - public declineProofRequest = (proof_record_id: string) => this._rest.declineProofRequest(proof_record_id); + public declineProofRequest = (proof_record_id: string) => + this._rest.declineProofRequest(proof_record_id); public resolveDid = (body: IdReqDto) => this._rest.resolveDid(body); public getCreatedDids = () => this._rest.getCreatedDids(); @@ -113,7 +137,15 @@ class ApiClient { }, }); - return fetch(url, init); + const response = await fetch(url, init); + + if (response.status === 401) { + if (config.onUnauthorized) { + config.onUnauthorized(); + } + } + + return response; } public jsonParseReviver = (key: string, value: unknown): unknown => { diff --git a/libs/config/src/config/agent.config.ts b/libs/config/src/config/agent.config.ts index 9922cae8..489e8376 100644 --- a/libs/config/src/config/agent.config.ts +++ b/libs/config/src/config/agent.config.ts @@ -15,7 +15,7 @@ export const agentConfig = registerAs( agentDbPass: process.env["AGENT_DB_PASS"]!, agentIsRest: process.env["AGENT_IS_REST"] === "true", agentConsumerName: process.env["AGENT_CONSUMER_NAME"]!, - agentConsumerMaxMessagess: + agentConsumerMaxMessages: parseInt(process.env["AGENT_MAX_MESSAGES"]!) || 10, agentConsumerRateLimit: parseInt(process.env["AGENT_RETE_LIMIT"]!) || 5, agentPort: parseInt(process.env["AGENT_PORT"]!), @@ -27,5 +27,12 @@ export const agentConfig = registerAs( agentSVDXWebHook: process.env["AGENT_SVDX_WEBHOOK_URL"]!, agentSVDXBasicUser: process.env["AGENT_SVDX_BASIC_USER"]!, agentSVDXBasicPass: process.env["AGENT_SVDX_BASIC_PASS"]!, + + agentAuthBasicEnabled: + !!process.env["AUTH_BASIC_USER"] && !!process.env["AUTH_BASIC_PASS"], + agentAuthBasicUser: process.env["AUTH_BASIC_USER"]!, + agentAuthBasicPass: process.env["AUTH_BASIC_PASS"]!, + agentAuthJwtEnabled: !!process.env["AUTH_JWT_PUBLIC_KEY"], + agentAuthJwtPublicKey: process.env["AUTH_JWT_PUBLIC_KEY"]!, }), ); diff --git a/libs/config/src/index.ts b/libs/config/src/index.ts index 1a892f81..630047fd 100644 --- a/libs/config/src/index.ts +++ b/libs/config/src/index.ts @@ -16,6 +16,7 @@ export * from "./interfaces/proof.manager.config.interface"; export * from "./schemas/nats.schema"; export * from "./schemas/agent.schema"; +export * from "./schemas/auth.schema"; export * from "./schemas/ledgers.schema"; export * from "./schemas/gateway.schema"; export * from "./schemas/connection.manager.schema"; diff --git a/libs/config/src/interfaces/agent.config.interface.ts b/libs/config/src/interfaces/agent.config.interface.ts index 59d42606..2027c8ba 100644 --- a/libs/config/src/interfaces/agent.config.interface.ts +++ b/libs/config/src/interfaces/agent.config.interface.ts @@ -9,7 +9,7 @@ export interface IConfAgent { agentDidSeed: string; agentIsRest: boolean; agentConsumerName: string; - agentConsumerMaxMessagess: number; + agentConsumerMaxMessages: number; agentConsumerRateLimit: number; agentPort: number; agentOobGoals: Array<string>; @@ -18,4 +18,10 @@ export interface IConfAgent { agentSVDXWebHook: string; agentSVDXBasicUser: string; agentSVDXBasicPass: string; + + agentAuthBasicEnabled: boolean; + agentAuthBasicUser: string; + agentAuthBasicPass: string; + agentAuthJwtEnabled: boolean; + agentAuthJwtPublicKey: string; } diff --git a/libs/config/src/schemas/agent.schema.ts b/libs/config/src/schemas/agent.schema.ts index 46b36990..364b4e3b 100644 --- a/libs/config/src/schemas/agent.schema.ts +++ b/libs/config/src/schemas/agent.schema.ts @@ -14,7 +14,5 @@ export const agentSchema = Joi.object({ AGENT_RETE_LIMIT: Joi.string().required(), AGENT_PORT: Joi.string().required(), AGENT_IS_SVDX: Joi.string().optional(), - AGENT_SVDX_SCHEMA_ID: Joi.string().optional(), - AGENT_SVDX_CRED_DEF_ID: Joi.string().optional(), AGENT_SVDX_WEBHOOK_URL: Joi.string().optional(), }); diff --git a/libs/config/src/schemas/auth.schema.ts b/libs/config/src/schemas/auth.schema.ts new file mode 100644 index 00000000..b8616bb4 --- /dev/null +++ b/libs/config/src/schemas/auth.schema.ts @@ -0,0 +1,7 @@ +import Joi from "joi"; + +export const authSchema = Joi.object({ + AUTH_BASIC_USER: Joi.string(), + AUTH_BASIC_PASS: Joi.string(), + AUTH_JWT_PUBLIC_KEY: Joi.string(), +}); diff --git a/libs/nats/src/consumer.nats.service.ts b/libs/nats/src/consumer.nats.service.ts index d6d6ce5d..a3ffc541 100644 --- a/libs/nats/src/consumer.nats.service.ts +++ b/libs/nats/src/consumer.nats.service.ts @@ -56,7 +56,7 @@ export class ConsumerService extends NatsBaseService { const consumer = await this.registerConsumer(this.streamConfig.name); const messages = await consumer.consume({ - max_messages: this.agentConfig.agentConsumerMaxMessagess, + max_messages: this.agentConfig.agentConsumerMaxMessages, }); for await (const message of messages) { diff --git a/package.json b/package.json index 377e36f2..426c6f54 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.1", "@nestjs/core": "^9.0.0", + "@nestjs/jwt": "^10.2.0", "@nestjs/microservices": "^9.4.2", "@nestjs/passport": "^10.0.1", "@nestjs/platform-express": "^9.0.0", diff --git a/yarn.lock b/yarn.lock index c7ef67c3..9ae15a04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1814,6 +1814,14 @@ path-to-regexp "3.2.0" tslib "2.5.3" +"@nestjs/jwt@^10.2.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/@nestjs/jwt/-/jwt-10.2.0.tgz#6aa35a04922d19c6426efced4671620f92e6dbd0" + integrity sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g== + dependencies: + "@types/jsonwebtoken" "9.0.5" + jsonwebtoken "9.0.2" + "@nestjs/mapped-types@1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@nestjs/mapped-types/-/mapped-types-1.2.2.tgz#d9ddb143776e309dbc1a518ac1607fddac1e140e" @@ -3465,6 +3473,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonwebtoken@9.0.5": + version "9.0.5" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz#0bd9b841c9e6c5a937c17656e2368f65da025588" + integrity sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA== + dependencies: + "@types/node" "*" + "@types/keyv@^3.1.4": version "3.1.4" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" @@ -4873,6 +4888,11 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" @@ -6258,6 +6278,13 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -9140,6 +9167,22 @@ jsonparse@^1.2.0, jsonparse@^1.3.1: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jsonwebtoken@9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" + integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^7.5.4" + "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: version "3.3.5" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" @@ -9160,6 +9203,23 @@ just-diff@^6.0.0: resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-6.0.2.tgz#03b65908543ac0521caf6d8eb85035f7d27ea285" integrity sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA== +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + keyv@^4.0.0, keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -9465,11 +9525,31 @@ lodash.escaperegexp@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" integrity sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw== +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg== + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA== + lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g== +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw== + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -9490,6 +9570,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -12476,7 +12561,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -- GitLab