diff --git a/package.json b/package.json index 571a4e2..96f4cf6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "SERVER_URL=https://4150-2a02-587-8701-de00-c100-84c1-e7c9-8738.ngrok-free.app node server.js" + "dev": "SERVER_URL=https://e756-2a02-587-8716-3600-5055-a1a8-76c2-c9a6.ngrok-free.app node server.js" }, "author": "", "license": "ISC", diff --git a/routes.js b/routes/routes.js similarity index 70% rename from routes.js rename to routes/routes.js index 4a6a1b4..cd46ce0 100644 --- a/routes.js +++ b/routes/routes.js @@ -5,18 +5,18 @@ import { pemToJWK, generateNonce, base64UrlEncodeSha256, -} from "./utils/cryptoUtils.js"; +} from "../utils/cryptoUtils.js"; import { buildAccessToken, generateRefreshToken, buildIdToken, -} from "./utils/tokenUtils.js"; +} from "../utils/tokenUtils.js"; import { SDJwtVcInstance } from "@sd-jwt/sd-jwt-vc"; import { createSignerVerifier, digest, generateSalt, -} from "./utils/sdjwtUtils.js"; +} from "../utils/sdjwtUtils.js"; import qr from "qr-image"; import imageDataURI from "image-data-uri"; @@ -44,8 +44,9 @@ const jwks = pemToJWK(publicKeyPem, "public"); //TODO move this into a service that caches these (e.g via redis or something) let sessions = []; let issuanceResults = []; -let codeFlowSessions = []; -let codeFlowSessionsResults = []; +let codeSessions = []; +let codeFlowRequests = []; +let codeFlowRequestsResults = []; router.get("/.well-known/openid-credential-issuer", async (req, res) => { // console.log("1 ROUTE /.well-known/openid-credential-issuer CALLED!!!!!!"); @@ -67,50 +68,93 @@ router.get( "/oauth-authorization-server/rfc-issuer", //this is required in case the issuer is behind a reverse proxy: see https://www.rfc-editor.org/rfc/rfc8414.html ], async (req, res) => { - // console.log("2 ROUTE /.well-known/oauth-authorization-server CALLED!!!!!!"); oauthConfig.issuer = serverURL; oauthConfig.authorization_endpoint = serverURL + "/authorize"; oauthConfig.token_endpoint = serverURL + "/token_endpoint"; oauthConfig.jwks_uri = serverURL + "/jwks"; - res.type("application/json").send(oauthConfig); } ); router.get(["/", "/jwks"], (req, res) => { - console.log("3 ROUTE ./jwks CALLED!!!!!!"); - res.json( - { - keys: [ - { ...jwks, kid: `aegean#authentication-key`, use: "sig" }, - { ...jwks, kid: `aegean#authentication-key`, use: "keyAgreement" }, //key to encrypt the sd-jwt response]) - ], - } - // res.json({ keys: jwks }); - ); + res.json({ + keys: [ + { ...jwks, kid: `aegean#authentication-key`, use: "sig" }, + { ...jwks, kid: `aegean#authentication-key`, use: "keyAgreement" }, //key to encrypt the sd-jwt response]) + ], + }); +}); + +///pre-auth flow +router.get(["/offer"], async (req, res) => { + const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); + sessions.push(uuid); + issuanceResults.push({ sessionId: uuid, status: "pending" }); + // console.log("active sessions"); + // console.log(issuanceResults); + let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer/${uuid}`; //OfferUUID + + let code = qr.image(credentialOffer, { + type: "png", + ec_level: "H", + size: 10, + margin: 10, + }); + let mediaType = "PNG"; + let encodedQR = imageDataURI.encode(await streamToBuffer(code), mediaType); + res.json({ + qr: encodedQR, + deepLink: credentialOffer, + sessionId: uuid, + }); }); +// auth code flow +router.get(["/offer-code"], async (req, res) => { + const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); + sessions.push(uuid); + issuanceResults.push({ sessionId: uuid, status: "pending" }); + console.log("active sessions"); + console.log(issuanceResults); //TODO associate this with a different "cache" + let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer-code/${uuid}`; + + let code = qr.image(credentialOffer, { + type: "png", + ec_level: "H", + size: 10, + margin: 10, + }); + let mediaType = "PNG"; + let encodedQR = imageDataURI.encode(await streamToBuffer(code), mediaType); + res.json({ + qr: encodedQR, + deepLink: credentialOffer, + sessionId: uuid, + }); +}); + +//pre-auth flow request router.get(["/credential-offer/:id"], (req, res) => { - //TODO assosiate the code with the credential offer res.json({ credential_issuer: serverURL, credentials: ["VerifiablePortableDocumentA1"], grants: { "urn:ietf:params:oauth:grant-type:pre-authorized_code": { - "pre-authorized_code": req.params.id, //"adhjhdjajkdk1hjhdj", + "pre-authorized_code": req.params.id, user_pin_required: true, }, }, }); }); +// auth code-flow request router.get(["/credential-offer-code/:id"], (req, res) => { res.json({ credential_issuer: serverURL, credentials: ["VerifiablePortableDocumentA1"], grants: { authorization_code: { - issuer_state: req.params.id, //"adhjhdjajkdk1hjhdj", + issuer_state: req.params.id, }, }, }); @@ -119,7 +163,7 @@ router.get(["/credential-offer-code/:id"], (req, res) => { router.get("/authorize", async (req, res) => { const responseType = req.query.response_type; const scope = req.query.scope; - const issuerState = decodeURIComponent(req.query.issuer_state); + const issuerState = decodeURIComponent(req.query.issuer_state); // This can be associated with the ITB session const state = req.query.state; const clientId = decodeURIComponent(req.query.client_id); //DID of the holder requesting the credential const authorizationDetails = JSON.parse( @@ -127,8 +171,8 @@ router.get("/authorize", async (req, res) => { ); const redirectUri = decodeURIComponent(req.query.redirect_uri); const nonce = req.query.nonce; - const codeChallenge = decodeURIComponent(req.query.code_challenge); //secret TODO cache this with challenge method under client state. - const codeChallengeMethod = req.query.code_challenge_method; //challenge method + const codeChallenge = decodeURIComponent(req.query.code_challenge); + const codeChallengeMethod = req.query.code_challenge_method; //this should equal to S256 const clientMetadata = JSON.parse( decodeURIComponent(req.query.client_metadata) @@ -144,7 +188,8 @@ router.get("/authorize", async (req, res) => { //EBSI style console.log(`credential ${authorizationDetails.types} was requested`); } else { - errors.push("no credentials requested"); + //errors.push("no credentials requested"); + console.log(`no credentials requested`); } } @@ -156,36 +201,60 @@ router.get("/authorize", async (req, res) => { } // If validations pass, redirect with a 302 Found response - const authorizationCode = "SplxlOBeZQQYbYS6WxSbIA"; //TODO make this dynamic - codeFlowSessions.push({ + const authorizationCode = generateNonce(16); //"SplxlOBeZQQYbYS6WxSbIA"; + codeFlowRequests.push({ challenge: codeChallenge, method: codeChallengeMethod, sessionId: authorizationCode, + issuerState: issuerState, }); - codeFlowSessionsResults.push({ + codeFlowRequestsResults.push({ sessionId: authorizationCode, + issuerState: issuerState, status: "pending", }); + codeSessions.push(issuerState); // push issuerState + + // for normal response not requesting VP from wallet + //const redirectUrl = `${redirectUri}?code=${authorizationCode}&state=${state}`; + + + //5.1.5. Dynamic Credential Request https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-12.html#name-successful-authorization-re + const redirectUrl = `http://localhost:8080?state=${state}&client_id=${clientId}&redirect_uri=${serverURL}/direct_post_vci&response_type=id_token& + response_mode=direct_post&scope=openid&nonce=${nonce}&request_uri=http://localhost:8080` + - const redirectUrl = `${redirectUri}?code=${authorizationCode}`; - // If there are errors, log errors if (errors.length > 0) { console.error("Validation errors:", errors); let error_description = ""; - error.array.forEach((element) => { + errors.forEach((element) => { error_description += element + " "; }); - const errorRedirectUrl = `${redirectUri}?error=invalid_request - &error_description=${error_description}`; + const encodedErrorDescription = encodeURIComponent( + error_description.trim() + ); + const errorRedirectUrl = `${redirectUri}?error=invalid_request&error_description=${encodedErrorDescription}`; + //TODO mark the codeFlowSession as failed return res.redirect(302, errorRedirectUrl); } else { - // This sets the HTTP status to 302 and the Location header to the redirectUrl return res.redirect(302, redirectUrl); } }); +router.post("/direct_post_vci", async (req, res) => { + console.log("direct_post VP for VCI is below!"); + let state = req.body["state"] + let jwt = req.body["id_token"]; + if (jwt) { + const authorizationCode = generateNonce(16); + const redirectUrl = `http://localhost:8080?code=${authorizationCode}&state=${state}`; + return res.redirect(302, redirectUrl); + } else { + return res.sendStatus(500); + } +}); + router.post("/token_endpoint", async (req, res) => { - // console.log("6 ROUTE /token_endpoint CALLED!!!!!!"); //pre-auth code flow const grantType = req.body.grant_type; const preAuthorizedCode = req.body["pre-authorized_code"]; // req.body["pre-authorized_code"] @@ -213,19 +282,12 @@ router.post("/token_endpoint", async (req, res) => { } } else { if (grantType == "authorization_code") { - //check PKCE - for (let i = 0; i < codeFlowSessions.array.length; i++) { - let element = codeFlowSessions.array[i]; - if (code === element.sessionId) { - let challenge = element.challenge; - // let method = element.method; - if (base64UrlEncodeSha256(code_verifier) === challenge) { - index = i; - codeFlowSessionsResults[i].status = "success"; - console.log("code flow" + issuanceResults[index].status); - } - } - } + validatePKCE( + codeFlowRequests, + code, + code_verifier, + codeFlowRequestsResults + ); } } //TODO return error if code flow validation fails and is not a pre-auth flow @@ -289,30 +351,34 @@ router.post("/credential", async (req, res) => { //issuerConfig.credential_endpoint = serverURL + "/credential"; //ITB -//TODO move these to a separate route router.get(["/issueStatus"], (req, res) => { let sessionId = req.query.sessionId; - // console.log("sessions found"); - // console.log(sessions); - // console.log("searching for sessionId" + sessionId); - // issuanceSession itbSession[sessionId] == offerUUID - let index = sessions.indexOf(sessionId); - console.log("index is"); - console.log(index); - if (index >= 0) { - let status = issuanceResults[index].status; - console.log(`sending status ${status} for session ${sessionId}`); - if (status === "success") { - sessions.splice(index, 1); - issuanceResults.splice(index, 1); - } - console.log(`new sessions`); - console.log(sessions); - console.log("new session statuses"); - console.log(issuanceResults); + // let index = sessions.indexOf(sessionId); + // console.log("index is"); + // console.log(index); + // if (index >= 0) { + // let status = issuanceResults[index].status; + // console.log(`sending status ${status} for session ${sessionId}`); + // if (status === "success") { + // sessions.splice(index, 1); + // issuanceResults.splice(index, 1); + // } + // console.log(`new sessions`); + // console.log(sessions); + // console.log("new session statuses"); + // console.log(issuanceResults); + + let result = + checkIfExistsIssuanceStatus(sessionId, sessions, issuanceResults) || + checkIfExistsIssuanceStatus( + sessionId, + codeSessions, + codeFlowRequestsResults + ); + if (result) { res.json({ - status: status, + status: result, reason: "ok", sessionId: sessionId, }); @@ -325,63 +391,38 @@ router.get(["/issueStatus"], (req, res) => { } }); -///credential-offer -router.get(["/offer"], async (req, res) => { - // console.log("4 ROUTE ./offer CALLED!!!!!!"); - const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); - sessions.push(uuid); - issuanceResults.push({ sessionId: uuid, status: "pending" }); - console.log("active sessions"); - console.log(issuanceResults); - // res.json({ - // request: `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer/${uuid}`, - // }); - - // generateGreOff... offerUUID - // itbSession.push({itbSession, offerUUID}) - - let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer/${uuid}`; //OfferUUID - - let code = qr.image(credentialOffer, { - type: "png", - ec_level: "H", - size: 10, - margin: 10, - }); - let mediaType = "PNG"; - let encodedQR = imageDataURI.encode(await streamToBuffer(code), mediaType); - res.json({ - qr: encodedQR, - deepLink: credentialOffer, - sessionId: uuid, - }); -}); - -router.get(["/offer-code"], async (req, res) => { - const uuid = req.query.sessionId ? req.query.sessionId : uuidv4(); - sessions.push(uuid); - issuanceResults.push({ sessionId: uuid, status: "pending" }); - console.log("active sessions"); - console.log(issuanceResults); - // res.json({ - // request: `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer/${uuid}`, - // }); - - let credentialOffer = `openid-credential-offer://?credential_offer_uri=${serverURL}/credential-offer-code/${uuid}`; - - let code = qr.image(credentialOffer, { - type: "png", - ec_level: "H", - size: 10, - margin: 10, - }); - let mediaType = "PNG"; - let encodedQR = imageDataURI.encode(await streamToBuffer(code), mediaType); - res.json({ - qr: encodedQR, - deepLink: credentialOffer, - sessionId: uuid, - }); -}); +function checkIfExistsIssuanceStatus(sessionId, sessions, sessionResults) { + let index = sessions.indexOf(sessionId); + console.log("index is"); + console.log(index); + if (index >= 0) { + let status = sessionResults[index].status; + console.log(`sending status ${status} for session ${sessionId}`); + console.log(`new sessions`); + console.log(sessions); + console.log("new session statuses"); + console.log(sessionResults); + if (status === "success") { + sessions.splice(index, 1); + sessionResults.splice(index, 1); + } + return status; + } + return null; +} + +async function validatePKCE(sessions, code, code_verifier, issuanceResults) { + for (let i = 0; i < sessions.length; i++) { + let element = sessions[i]; + if (code === element.sessionId) { + let challenge = element.challenge; + let tester = await base64UrlEncodeSha256(code_verifier); + if (tester === challenge) { + issuanceResults[i].status = "success"; + console.log("code flow status:" + issuanceResults[i].status); + } + } + } +} export default router; diff --git a/verifierRoutes.js b/routes/verifierRoutes.js similarity index 99% rename from verifierRoutes.js rename to routes/verifierRoutes.js index 8c8182b..32a8e1b 100644 --- a/verifierRoutes.js +++ b/routes/verifierRoutes.js @@ -6,7 +6,7 @@ import { generateNonce, decryptJWE, buildVpRequestJwt, -} from "./utils/cryptoUtils.js"; +} from "../utils/cryptoUtils.js"; import { decodeSdJwt, getClaims } from "@sd-jwt/decode"; import { digest } from "@sd-jwt/crypto-nodejs"; import qr from "qr-image"; @@ -124,6 +124,16 @@ verifierRouter.post("/direct_post/:id", async (req, res) => { } }); + + + + + + + + + + verifierRouter.get(["/verificationStatus"], (req, res) => { let sessionId = req.query.sessionId; let index = sessions.indexOf(sessionId); diff --git a/server.js b/server.js index ffaa52e..90bd468 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,7 @@ // main file import express from "express"; -import router from "./routes.js"; -import verifierRouter from "./verifierRoutes.js"; +import router from "./routes/routes.js"; +import verifierRouter from "./routes/verifierRoutes.js"; import bodyParser from 'body-parser'; // Body parser middleware