Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ Releases of all packages are available here: [Releases](https://github.com/ibm-s
The following components are currently offered in the package.
| Component | Description |
| ----------- | ----------- |
| [Privacy](sdk/privacy) | Fast, opinionated, simple privacy component that leverages the data privacy & consent engine on IBM Security Verify. |
| [Privacy](sdk/privacy) | Fast, opinionated, simple privacy component that leverages the data privacy & consent engine on IBM Verify. |
| [Examples](examples/) | Collection of **demo applications** that illustrate common server-side scenarios supporting mobile identity, authentication, and digital credential use cases. |

### Installation

Expand Down
18 changes: 18 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Mobile Scenario Demo Apps

This repository contains a collection of **JavaScript demo applications**`** that illustrate common server-side scenarios supporting mobile identity, authentication, and digital credential use cases.

Each demo app is designed to showcase specific integration patterns and protocols that can be used with mobile SDKs or clients implementing Digital Credentials (DC), OAuth 2.0, OpenID Connect, and related standards.


## Demo Apps

| Name | Description |
|-----------|-------------|
| **dpop** | Demonstrates how to implement **Demonstration of Proof-of-Possession (DPoP)** for securing OAuth 2.0 access tokens. Shows how to generate and verify DPoP proofs on the server side. [Read the documentation →](./dpop/README.md) |

## Prerequisites

- [Node.js](https://nodejs.org/) (version 18 or higher recommended)
- npm or yarn
- Basic understanding of OAuth 2.0, OpenID Connect, and digital credential flows
14 changes: 14 additions & 0 deletions examples/dpop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
This folder contains a custom web application used to demonstrate configuring DPoP flows with IBM Verify.

# 🚀 Getting Started

Clone this repository and install dependencies for a specific demo:

1. `git clone https://github.com/IBM-Verify/verify-mobile-js.git`
1. `cd verify-mobile-js/dpop`
1. Configure the relevant parameter in `app.js`
1. `npm install`
1. `node app.js`


They are supporting assets for articles published on [IBM Verify Docs](https://docs.verify.ibm.com/verify/docs/best-practices-demonstration-proof-of-possession) or on IBM Verify community blogs.
208 changes: 208 additions & 0 deletions examples/dpop/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import express from 'express'
import jose from 'node-jose'
import crypto from 'crypto'
import https from 'https'
import fs from 'fs'
import { time } from 'console';

const app = express();

const key = fs.readFileSync('./key.pem');
const cert = fs.readFileSync('./cert.pem');

// Change these parameters according to your IBM Security Verify tenant
const clientId = "<your_clientid>"
const clientSecret = "<your_clientsecret>"
const authorizationHeader = "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
const tenant = "<your_tenant>" // without protocol

const port = 8080
const introspectEndpoint = `/oauth2/introspect`

let dpopProof = true
let computedFingerprint = ''
let tokenCache = []

app.get("/", (request, response) => {
response.send("Web app to demonstrate IBM Verify Identity high assurance flows")
})

app.get("/validate-token", async (request, response) => {

computedFingerprint = ''
dpopProof = true

console.log("-------------------------------------------------------------------------------------------")
const accessToken = request.headers.authorization.split(" ")[1]
const dpopHeader = request.headers["dpop"]
// console.log("Access token: " + accessToken)
// console.log("DPoP header: " + request.headers["dpop"])

await validateDpopHeader(request, dpopHeader, accessToken)

if (dpopProof) {
const timeInSec = new Date().getTime() / 1000
const cachedTokenExp = tokenCache[accessToken]

if ((cachedTokenExp != undefined) && (timeInSec < cachedTokenExp)) {
console.log("Token found in cache: %s.", accessToken)
console.log("Valid until: %s", new Date(cachedTokenExp * 1000))
console.log("Current time: %s", new Date(timeInSec * 1000))
} else {
const responseText = await doTokenInspectionRequest(accessToken)

const introspectionResponse = JSON.parse(responseText)
dpopProof = dpopProof && (introspectionResponse["cnf"]["jkt"] !== undefined)
dpopProof = dpopProof && (introspectionResponse["cnf"]["jkt"] === computedFingerprint)
console.log("JWK fingerprint match: " + (introspectionResponse["cnf"]["jkt"] === computedFingerprint))

if (dpopProof) {
console.log("Store token %s in cache. Expires at: %s", accessToken, new Date(introspectionResponse["exp"] * 1000))
tokenCache[accessToken] = introspectionResponse["exp"]
} else {
console.log("Remove token %s from cache", accessToken)
tokenCache[accessToken] = undefined
}
}
}

console.log("Authorization result: " + dpopProof.toString().toUpperCase())
console.log("###########################################################################################")
if (dpopProof) {
response.sendStatus(204)
} else {
response.sendStatus(401)
}
})

async function doTokenInspectionRequest(accessToken) {
return new Promise((resolve, reject) => {
const options = {
hostname: tenant,
port: 443,
path: introspectEndpoint,
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': authorizationHeader
}
}

const payload = `token=${accessToken}&token_type_hint=DPoP`

let responseData = []
// console.log("Request options: " + JSON.stringify(options))
// console.log("Request payload: " + payload)

const request = https.request(options, (response) => {
response.on('data', chunk => {
responseData.push(chunk)
})

response.on('end', function () {
const responseBody = Buffer.concat(responseData).toString()
console.log("Token introspection: " + responseBody)
resolve(responseBody)
})
})

request.write(payload)

request.on('error', error => {
console.log('> Error: ', error.message)
dpopProof = false
reject(error)
})

request.end();
})
}

async function validateDpopHeader(request, dpopHeader, accessToken) {

try {
dpopProof = dpopProof && (request.headers["dpop"].split(',').length == 1)
console.log("There is only one DPoP HTTP request header field: " + dpopProof)

// The DPoP HTTP request header field value is a single and well-formed JWT.
// The JWT signature verifies with the public key contained in the jwk JOSE Header Parameter
let dpopHeaderUnpacked = await jose.JWS.createVerify().verify(dpopHeader, { allowEmbeddedKey: true })
let jsonPayload = JSON.parse(dpopHeaderUnpacked.payload)
let htu = jsonPayload.htu
let htm = jsonPayload.htm
let ath = jsonPayload.ath
let jti = jsonPayload.jti
let iat = jsonPayload.iat
let exp = iat + 60

// All required claims per Section 4.2 are contained in the JWT.
dpopProof = dpopProof && (htu !== undefined)
dpopProof = dpopProof && (htm !== undefined)
dpopProof = dpopProof && (ath !== undefined)
dpopProof = dpopProof && (jti !== undefined)
dpopProof = dpopProof && (iat !== undefined)
console.log("All required claims per Section 4.2 are contained in the JWT: " + dpopProof)

// The typ JOSE Header Parameter has the value dpop+jwt.
dpopProof = dpopProof && (dpopHeaderUnpacked.header.typ === "dpop+jwt")
console.log("The typ JOSE Header Parameter has the value dpop+jwt: " + (dpopHeaderUnpacked.header.typ === "dpop+jwt"))

// The alg JOSE Header Parameter indicates a registered asymmetric digital signature algorithm.
dpopProof = dpopProof && (dpopHeaderUnpacked.header.alg === "RS256")
console.log("The alg JOSE Header Parameter indicates a registered asymmetric digital signature algorithm: " + (dpopHeaderUnpacked.header.alg === "RS256"))

console.log("DPoP header payload: " + JSON.stringify(jsonPayload))
console.log("Url: " + htu)
console.log("Method DPoP: " + htm)
console.log("Method request: " + request.method)

// The htm claim matches the HTTP method of the current request.
dpopProof = dpopProof && (htm === request.method)
console.log("The htm claim matches the HTTP method of the current request: " + (htm === request.method))

// The htu claim matches the HTTP URI value for the HTTP request in which the JWT was received
const fullUrl = request.protocol + '://' + request.get('host') + request.originalUrl
dpopProof = dpopProof && (htu === fullUrl)
console.log("HTU: " + htu)
console.log("URL: " + fullUrl)
console.log("The htu claim matches the HTTP URI value for the HTTP request in which the JWT was received: " + (htu === fullUrl))

// The creation time of the JWT ... is within an acceptable window.
const timeInSec = new Date().getTime() / 1000
dpopProof = dpopProof && (iat < timeInSec + 1) && (exp > timeInSec)
console.log("iat " + new Date(iat * 1000))
console.log("cts " + new Date((timeInSec + 1) * 1000))
console.log("exp " + new Date(exp * 1000))
console.log("The creation time of the JWT ... is within an acceptable window of 60 seconds: " + ((iat < (timeInSec + 1)) && (exp > timeInSec)))

let digest = crypto.createHash('sha256').update(accessToken).digest()
let atHash = jose.util.base64url.encode(digest);
console.log("ath (calculated): " + atHash)
console.log("ath (from DPoP header): " + ath)

dpopProof = dpopProof && (atHash === ath)
console.log("DPoP header is associated with the access token: " + (atHash === ath))

let thumbprint = await dpopHeaderUnpacked.key.thumbprint('SHA-256');
computedFingerprint = jose.util.base64url.encode(thumbprint);
console.log("JWK fingerprint: " + computedFingerprint)

} catch (error) {
dpopProof = false
console.log(error)
}
}

app.get("/status", (request, response) => {
const status = {
"Status": "Running"
}

response.send(status)
})

const server = https.createServer({key: key, cert: cert }, app);
server.listen(8080, () => {
console.log(`SERVER STARTED ON localhost:${port}`);
})
21 changes: 21 additions & 0 deletions examples/dpop/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDXjCCAkYCCQC9EfBtFzDtZjANBgkqhkiG9w0BAQsFADBxMQswCQYDVQQGEwJB
VTEMMAoGA1UECAwDUUxEMRMwEQYDVQQHDApHb2xkIENvYXN0MRUwEwYDVQQKDAxJ
Qk0gU2VjdXJpdHkxFDASBgNVBAsMC0RldmVsb3BtZW50MRIwEAYDVQQDDAlsb2Nh
bGhvc3QwHhcNMjMxMDMxMTAwMDE5WhcNMjYwNzI3MTAwMDE5WjBxMQswCQYDVQQG
EwJBVTEMMAoGA1UECAwDUUxEMRMwEQYDVQQHDApHb2xkIENvYXN0MRUwEwYDVQQK
DAxJQk0gU2VjdXJpdHkxFDASBgNVBAsMC0RldmVsb3BtZW50MRIwEAYDVQQDDAls
b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC17TRiDYiq
FiRDtXwY/V3xaPZR1h3gHSrkKNFLNaT21cem6MVwMODG9aWgPbxKufBZCJ0i+xax
iGv0VUZ6vayxXmRC5ByTzLZGhW9s6AyUfcx7+sOuFxx20IwjNheGTe8K0E/tK84u
IG2Yy2wvBIjSyw0u+lNjhVB0+p6SclM8s+yMPkyfu3l7H3x/vdpdx3/HjDmkTLu3
1Et2cLTavVzMc6LPVv1HIOWhNvTQBlVCY6LqVIwU9D5L0warnDfqxTgBuKF/RECy
47H4lQP5O5Gin84cDmckO+Ji6BH2pgyEt1h//8TK5DXNoTZajArtdzjxYkxB/cA4
YdJRDJniooGdAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAprcZTALXSW4wJ9DHeW
zvLu/CyJzJtOXsLh2DpkKyJ0S3C6OjGTd2WbKf7aIKTKDzafa7xPl6ptccbY1pVO
b2RWLPz2nPrDtHyau1GJKL+rQvOqKSIifNv+Qrh16Pp/F5hY3OdanujzztdqrDUr
TjDDmYoASGE2hCDB9MwEAV7vb+64JWVZ2Hsqjv2bKOGJv/MRTn0ub2UYl+YM1ZLS
ZeqVYMrjskpGQQG3U4GcI6dv3G93GDNRxiEnsfd0SHaDofn4MWi+I2I0JKt2G9w4
tCE8kNTjOBdLToUPeAd/5d3k6xGRf3MflD2ADJX5th1MOD4U049AFecYZ16GX1AS
/JU=
-----END CERTIFICATE-----
1 change: 1 addition & 0 deletions examples/dpop/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
const PORT = process.env.PORT || 8080;
27 changes: 27 additions & 0 deletions examples/dpop/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAte00Yg2IqhYkQ7V8GP1d8Wj2UdYd4B0q5CjRSzWk9tXHpujF
cDDgxvWloD28SrnwWQidIvsWsYhr9FVGer2ssV5kQuQck8y2RoVvbOgMlH3Me/rD
rhccdtCMIzYXhk3vCtBP7SvOLiBtmMtsLwSI0ssNLvpTY4VQdPqeknJTPLPsjD5M
n7t5ex98f73aXcd/x4w5pEy7t9RLdnC02r1czHOiz1b9RyDloTb00AZVQmOi6lSM
FPQ+S9MGq5w36sU4Abihf0RAsuOx+JUD+TuRop/OHA5nJDviYugR9qYMhLdYf//E
yuQ1zaE2WowK7Xc48WJMQf3AOGHSUQyZ4qKBnQIDAQABAoIBADpgwZxtR1t8+2oW
xJmoRAsBaXldQfz2nxrT1kVSE3t5ojV3IFQd+yMdFGbVKKS6AdwwHWxqMiAJ7Mpc
yt78GnxYE8g7NsheumbqzpSd6duEeqeWElC87c9aoH10EBxyybAopF0w31qB4WlU
bYSw6c7qhXJ7tWKMFwNSoKvRr9AcSdGGzQx/lOVPTqDxtEQdfX6mIApa1lLkAnHM
8qou/X1kMNsugv9ci6Vti+B7/JVK8HUtYtY1feIbAyqhhFXrSsDUPFwi95Eb03JL
OnWaD2n67Lh6axiOFcCB5LbIdK/gsDdDb/w9Ax4I4u3CLaz6oc5UJWDRUlge8FKT
rOYB2CkCgYEA3WYaoFqSFxDx35YBJShBgOve0Y7bsAaxhXJioYosUR3pkFy7ie2T
t916GXiyo6grU+Z28PvKl9Xri64atILg6m4rG5fmX8QbksW0xY/CTYDBSyzEPFQ4
3/BFWSkmL3OXJTA+y5X3PG9tl/ZaQyiaeQoFQz5Rlsw57BxDpdASHKsCgYEA0lvd
IpOlYwlyx/p1ewYf1//7bXuWYmqTmgyI66pVyoCgWIYLDR8sx65x1JKQFWf5q0Lm
WJCsjviuUxEe4AzExDz3Pney8qGpEuDL+oZXiMm7XxSAS0EPnfz/zokfBVsSnTfN
J9ui2QxAm5uqeKE9d6gviSkucsjHBTqnBW9pStcCgYEAhhCfDPzrO+Z54J9QADUZ
PT0XRyRPqY7UDm/Og+1Mmq4XUeCle4cOjScjGat52RorannDCngeHMVgBcwexbH/
ClY96k6YJON2ovDvXzaHPTE7Ww83oSSK8Cfphm0hf1hqbQ2C6PrdpI4A+iUmUUaU
C7liqG5jL4JpjK0s1YpifVMCgYA5kGXtJlYlydodG062wbBJHYWiKiW2/M8zYqa9
Rrl/Vr+KOfQgPR217ui7cPf4w6Ew2nfKWJy/6xFZLeAzE4ts3/oQoBTaDJ8FyXpI
LicaCYo6tJN/BGjPpQIjdKaGgquPVkvP0my31ICBlJGLvSPi8KVBdYF+a676oVg/
RoObtQKBgH2lXDo7mQkZLsJoe8ob4ssu9Kb8gaCiHNptwhydubcdF0rK47l+yMUM
ZIe4CMAW3Ley/ru3CcKgRry9FRRULv1D9kZ+CbPhp7cnWwl6/6NglaRg1rc+tXte
Zob6zpcaFDC/ngwhip00DbbtPqUHqJXSBId4HQ0VkOQT+ycqH4o9
-----END RSA PRIVATE KEY-----
22 changes: 22 additions & 0 deletions examples/dpop/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "dpop-demo-app",
"version": "1.0.0",
"description": "Demo for DPoP token handling",
"main": "app.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.20.2",
"ejs": "^3.1.9",
"expres": "^0.0.5",
"express": "^4.18.2",
"https": "^1.0.0",
"node-jose": "^2.2.0",
"nodemon": "^3.0.1",
"openid-client": "^5.6.1"
}
}