This library provides utilities that help with authentication against the Veracity Identity Provider. It's based on the robust libraries passport
and the strategy passport-azure-ad
.
- PassportJS authentication strategy for web applications using the Veracity Identity Provider.
- Helper for setting up authentication with Veracity as well as logout.
- Middleware for refreshing tokens.
- Support for encrypting session data at-rest.
- Installation
- Examples
- Usage
- Passing state
- Check if the user is logged in
- Authentication process
- Refresh token
- Logging out
- Error handling
- Logging
- Data structures
- IDefaultAuthConfig
- IFullAuthConfig
- IExtraAuthenticateOptions
- ILoggerLike
- IRouterLike
- ISetupWebAppAuthSettings
- IVIDPAccessTokenPayload
- IVIDPAccessTokenData
- IVIDPAccessToken
- IVIDPWebAppStrategySettings
- IVIDPJWTTokenHeader
- IVIDPJWTTokenData
- IVIDPJWTTokenPayloadCommonClaims
- IVIDPJWTToken
- VIDPRequestErrorCodes
- VIDPAccessTokenErrorCodes
- VIDPTokenValidationErrorCodes
- VIDPStrategyErrorCodes
- VIDPRefreshTokenErrorCodes
- Helpers
Run npm i @veracity/node-auth
or yarn add @veracity/node-auth
to install. TypeScript types are included in the package.
See one of the examples to get started.
The helper setupWebAppAuth
simplifies setting up authentication towards Veracity. If you're looking for an alternative, you can use passport
in combination with passport-azure-ad
.
const express = require("express")
const https = require("https")
const app = express()
const { setupWebAppAuth, generateCertificate } = require("@veracity/node-auth")
const { MemoryStore } = require("express-session")
const { refreshTokenMiddleware } = setupWebAppAuth({
app,
session: {
secret: "your-long-super-secret-secret-here",
store: new MemoryStore() // Notice! Only use MemoryStore for development
},
strategy: { // Fill these in with values from your Application Credential
clientId: "<your-client-id>",
clientSecret: "<your-client-secret>",
replyUrl: "https://localhost:3000/auth/oidc/loginreturn" // make sure to update with your own replyUrl
}
})
// This endpoint will return our user data so we can inspect it.
app.get("/user", (req, res) => {
if (req.isAuthenticated()) {
res.send(req.user)
return
}
res.status(401).send("Unauthorized")
})
// Create an endpoint where we can refresh the services token.
app.get("/refresh", refreshTokenMiddleware(), (req, res) => {
res.send("Refreshed token!")
})
// Set up the HTTPS development server
const server = https.createServer({
...generateCertificate() // Generate self-signed certificates for development
}, app)
server.on("error", (error) => { // If an error occurs halt the application
console.error(error)
process.exit(1)
})
server.listen(3000, () => { // Begin listening for connections
console.log("Listening for connections on port 3000")
})
setupWebAppAuth
register the following default routes:
/login
: Path used to initialize the login process/logout
: Path used to log out the user and delete session data/error
: Where the user is redirected if there are errors in the login process
Sometimes it is useful to be able to pass data from before the login begins all the way through the authentication process until control is returned back to your code. This library supports this in two ways for web and native applications (the bearer token validation strategy does not support this):
- Any query parameters sent to the login handler are mirrored onto the request object when the login completes. This means you can inspect
req.query
in the POST handler (oronLoginComplete
) when the authentication completes and see the same ones that were sent to the login request. Returning query parameters from Veracity IDP will take presedence. - You can modify the request object in a handler before beginning the authentication process by adding data to the
veracityAuthState
property. Any data found here will be mirrored onto the final request object once the login completes.req.veracityAuthState
before login will equalreq.veracityAuthState
after login.
To pass state using the helper functions for web and native applications you simply provide an onBeforeLogin
and an onLoginComplete
function to set the state and read the state respectively. Since query parameters are always mirrored if you only want to use those you do not need to provide an onBeforeLogin
function at all.
// Setup is identical for setupNativeAppAuth
setupWebAppAuth({
// ... other settings are omitted for brevity
onBeforeLogin: (req, res, next) => {
req.veracityAuthState = JSON.stringify({redirect: "/here"})
next()
},
onLoginComplete: (req, res, next) => {
const state = JSON.parse(req.veracityAuthState)
res.redirect(state.redirect || "/")
}
})
Call the method req.isAuthenticated()
to see if the user is logged in. Returns a boolean
.
The authentication process used by Veracity is called Open ID Connect with token negotiation using Authorization Code Flow. Behind the scenes, Veracity relies on Microsoft Azure B2C to perform the actual login. You can read more about the protocol on Microsoft's website.
This library provides you with a strategy that you can use to perform authentication. The strategy is compatible with PassportJS and allows any Connect-compatible library to authenticate with Veracity. The technicalities of the protocol are then handled by the library and you can focus on utilizing the resulting tokens to call APIs and build your application.
The library also gives you a way to refresh the token. The method is returned to you when calling the setupWebAppAuth
method:
const { refreshTokenMiddleware } = setupWebAppAuth(/* ... config ... */)
Later, use the refreshTokenMiddleware
method like this:
app.get("/refresh", refreshTokenMiddleware(), (req, res, next) => {
res.send("OK, token refreshed")
})
If you provide your own onVerify
function to setupWebAppAuth
and want to store the tokens in some specific location, you can pass in the optional arguments for resolving refresh token and storing the access token like so:
const resolveRefreshToken = (req) => req.user.customTokenPlacement.refreshToken
const storeRefreshedTokens = (refreshResponse, req) => {
req.user.customTokenPlacement = {
accessToken: refreshResponse.access_token,
refreshToken: refreshResponse.refresh_token
}
}
app.get("/refresh", refreshTokenMiddleware(resolveRefreshToken, storeRefreshedTokens), (req, res, next) => {
res.send("OK, token refreshed")
})
Logging out users is a relatively simple process. Your application is storing session information (user data including access tokens) within some kind of session storage. This must be removed. Then you need to redirect users to the sign out page on Veracity to centrally log them out. This last step is required by Veracity to adhere to security best-practices. The logout url is set up bt default to "/logout", but you can change it by passing logoutPath
in the configuration object of setupWebAppAuth
.
The URL on Veracity where you should direct users logging out is stored as a constant in this library:
const { VERACITY_LOGOUT_URL } = require("@veracity/node-auth")
app.get("/logout", (req, res) => {
req.logout()
res.redirect(VERACITY_LOGOUT_URL)
})
Any error that occurs within a strategy provided by this library (or by extension a helper function) will be an instance of a VIPDError
. VIDPError objects are extensions of regular Error objects that contain additional information about what type of error occured. Using this information you can decide how to proceed.
The properties of a VIDPError
object are:
Property | Type | Description |
---|---|---|
code | string | A unique code for this error corresponding to an error code from any of the *ErrorCodes interfaces (see below). |
description | string | A more detailed description of the error useful for debugging. |
source | string | A source for where the error occured within the library. |
details? | any | An object defining more details about the error in a machine readable format. |
innerError? | Error | If this error instance was created based on another error this property will contain that specific error instance. |
Should an error occur during any part of the authentication process it will be passed to next() just like other errors in Connect-compatible applications like ExpressJS. You should handle these errors according to the documentation from your library of choice. You can find more information on error handling in Connect here and ExpressJS here.
const { VIDPError } = require("@veracity/node-auth")
// Register an error handler in your application
app.use((err, req, res, next) => {
if (err instanceof VIDPError) { // This is an error that occured with the Veracity Authentication strategy
// Check err.details.error for the type and act accordingly
}
})
You can pass in a custom logger when using setupWebAppAuth
. Example:
const express = require("express")
const { MemoryStore } = require("express-session")
const { setupWebAppAuth } = require("../../dist")
const winston = require("winston")
const app = express()
setupWebAppAuth({
app,
strategy: {
clientId: "...",
clientSecret: "...",
replyUrl: "https://localhost:3000/auth/oidc/loginreturn"
},
session: {
secret: "...",
store: new MemoryStore()
},
logger: winston.createLogger({
transports: [
new winston.transports.Console(),
]
})
})
⭐️
The library makes use of several data structures. They are all defined as TypeScript interfaces that will be visible in any TypeScript aware editor. Below is an export of all public types.
Property | Type | Description |
---|---|---|
loginPath | string | |
logoutPath | string | |
errorPath | string | |
logLevel | LogLevel | |
name | string | |
oidcConfig | Omit<IOIDCStrategyOption, "clientID" | "redirectUrl"> | |
policyName | string | |
tenantID | string | |
onLogout | (req: Request, res: Response, next: NextFunction) => void | |
onBeforeLogin | (req: Request, res: Response, next: NextFunction) => void | |
onVerify | VerifyOIDCFunctionWithReq | |
onLoginComplete | (req: Request, res: Response, next: NextFunction) => void |
extends Omit<IDefaultAuthConfig, "oidcConfig">
Property | Type | Description |
---|---|---|
oidcConfig | IOIDCStrategyOption | |
session | IMakeSessionConfigObjectOptions | |
additionalAuthenticateOptions? | IExtraAuthenticateOptions | Additional options passed to passport.authenticate |
Property | Type | Description |
---|---|---|
extraAuthReqQueryParams? | {[key: string]: string | number | boolean} |
Property | Type | Description |
---|---|---|
info | (str: any) => void | |
warn | (str: any) => void | |
error | (str: any) => void | |
levels? | (str: any) => void |
extends Pick<Router, "use" | "get" | "post">
Property | Type | Description |
---|
Property | Type | Description |
---|---|---|
app | IRouterLike | The express application to configure or the router instance. |
errorPath? | string | Where to redirect user on error |
loginPath? | string | The path where login will be configured |
logoutPath? | string | The path where logout will be configured |
logLevel? ="error" |
LogLevel | Logging level |
session | IMakeSessionConfigObjectOptions | Session configuration for express-session |
strategy | IVIDPWebAppStrategySettings | Configuration for the strategy you want to use. |
name? ="veracity-oidc" |
string | Name of the passport strategy |
policyName? ="B2C_1A_SignInWithADFSIdp" |
string | Policy to use when logging in. |
onBeforeLogin? | (req: Request & {veracityAuthState?: any}, res: Response, next: NextFunction) => void | Provide a function that executes before the login process starts. It executes as a middleware so remember to call next() when you are done. |
onVerify? | VerifyOIDCFunctionWithReq | The verifier function passed to the strategy. If not defined will be a passthrough verifier that stores everything from the strategy on req.user . |
onLoginComplete? | (req: Request, res: Response, next: NextFunction) => void, | A route handler to execute once the login is completed. The default will route the user to the returnTo query parameter path or to the root path. |
onLogout? | (req: Request & {veracityAuthState?: any}, res: Response, next: NextFunction) => void, | A route handler to execute once the user tries to log out. The default handler will call req.logout() and redirect to the default Veracity central logout endpoint. |
logger? | ILoggerLike | Optional provide your own logger |
extends IVIDPJWTTokenPayloadCommonClaims
Property | Type | Description |
---|---|---|
azp | string | |
userId | string | The users unique ID within Veracity. |
dnvglAccountName | string | The account name for the user. |
myDnvglGuid⬇ | string | Deprecated: - The old id for the user. |
oid | string | An object id within the Veracity IDP. Do not use this for user identification @see userId |
upn | string | |
scp | string |
extends IVIDPJWTTokenData
Property | Type | Description |
---|---|---|
scope | string | The scope this token is valid for. |
refreshToken? | string | If a refresh token was negotiated it will be contained here. |
extends IVIDPJWTToken
Property | Type | Description |
---|
Property | Type | Description |
---|---|---|
clientId | string | The client id from the Application Credentials you created in the Veracity for Developers Provider Hub. |
clientSecret? | string | The client secret from the Application Credentials you created in the Veracity for Developers Provider Hub. Required for web applications, but not for native applications. |
replyUrl | string | The reply url from the Application Credentials you created in the Veracity for Developers Provider Hub. |
apiScopes? =["https://dnvglb2cprod.onmicrosoft.com/83054ebf-1d7b-43f5-82ad-b2bde84d7b75/user_impersonation"] |
string[] | The scopes you wish to authenticate with. An access token will be retrieved for each api scope. If you only wish to authenticate with Veracity you can ignore this or set it to an empty array to |
slightly improve performance. | ||
metadataURL? =VERACITY_METADATA_ENDPOINT |
string | The url where metadata about the IDP can be found. Defaults to the constant VERACITY_METADATA_ENDPOINT. |
additionalAuthenticateOptions? | IExtraAuthenticateOptions | Additional options passed to passport.authenticate |
Property | Type | Description |
---|---|---|
typ | string | The type of token this is. |
alg | string | The message authentication code algorithm. |
kid | string | The id of the key used to sign this token. |
Property | Type | Description |
---|---|---|
token | string | The full token string |
header | IVIDPJWTTokenHeader | Header information from the token |
payload | TPayload | The token payload |
issued | number | Unix timestamp for when the token was issued. |
lifetime | number | The number of seconds this token is valid for. |
expires | number | Unix timestamp for when the token expires. |
Property | Type | Description |
---|---|---|
iss | string | Issuer |
sub | "Not supported currently. Use oid claim." | Subject |
aud | string | Audience |
exp | number | Expiration time. |
nbf | number | Not valid before time. |
iat | number | Issued at time. |
string[] | ||
nonce | string | |
given_name | string | |
family_name | string | |
name | string | |
ver | "1.0" |
Property | Type | Description |
---|---|---|
header | IVIDPJWTTokenHeader | |
payload | TPayload | |
signature | string |
Property | Type | Description |
---|---|---|
"read_timeout" | "read_timeout" | A timeout occured when waiting to read data from the server. |
"connect_timeout" | "connect_timeout" | A timeout occurred when waiting to establish a connection to the server. |
"status_code_error" | "status_code_error" | The request returned a non 200 status code. |
Property | Type | Description |
---|---|---|
"invalid_request" | "invalid_request" | Protocol error, such as a missing required parameter. |
"invalid_grant" | "invalid_grant" | The authorization code or PKCE code verifier is invalid or has expired. |
"unauthorized_client" | "unauthorized_client" | The authenticated client isn't authorized to use this authorization grant type. |
"invalid_client" | "invalid_client" | Client authentication failed. |
"unsupported_grant_type" | "unsupported_grant_type" | The authorization server does not support the authorization grant type. |
"invalid_resource" | "invalid_resource" | The target resource is invalid because it does not exist, Azure AD can't find it, or it's not correctly configured. |
"interaction_required" | "interaction_required" | The request requires user interaction. For example, an additional authentication step is required. |
"temporarily_unavailable" | "temporarily_unavailable" | The server is temporarily too busy to handle the request. |
Property | Type | Description |
---|---|---|
"malfomed_token" | "malfomed_token" | The token is malformed. It may not consist of three segments or may not be parseable by the jsonwebptoken library. |
"missing_header" | "missing_header" | The token is malformed. Its header is missing. |
"missing_payload" | "missing_payload" | The token is malformed. Its payload is missing. |
"missing_signature" | "missing_signature" | The token is malformed. Its signature |
"no_such_public_key" | "no_such_public_key" | The token requested a public key with an id that does not exist in the metadata endpoint. |
"verification_error" | "verification_error" | An error occured when verifying the token against nonce, clientId, issuer, tolerance or public key. |
"incorrect_hash" | "incorrect_hash" | The token did not match the expected hash |
Property | Type | Description |
---|---|---|
"missing_required_setting" | "missing_required_setting" | A required setting was missing. See description for more information. |
"invalid_internal_state" | "invalid_internal_state" | The internal state of the system is not valid. This may occur when users peforms authentication too slowly or if an attacker is attempting a replay attack. |
"verifier_error" | "verifier_error" | An error occured in the verifier function called once the authentication is completed. |
"unknown_error" | "unknown_error" | This error code occurs if the system was unable to determine the reason for the error. Check the error details or innerError for more information. |
Property | Type | Description |
---|---|---|
"cannot_resolve_token" | "cannot_resolve_token" | Token refresh middleware was unable to resolve the token using the provided resolver. See description for more details. |
When configuring authentication you need to provide a place to store session data. This is done through the store
configuration for express-session
. In the samples we use a MemoryStore instance that keeps the data in memory, but this is not suitable to for production as it does not scale. For such systems you would probably go with a database or cache of some kind such as MySQL or Redis.
Once you set up such a session storage mechanism, however there are some considerations you need to take into account. Since the access tokens for individual users are stored as session data it means that anyone with access to the session storage database can extract any token for a currently logged in user and use it themselves. Since the token is the only key needed to perform actions on behalf of the user it is considered sensitive information and must therefore be protected accordingly.
This library comes with a helper function to deal with just this scenario called createEncryptedSessionStore
. This function uses the AES-256-CBC algorithm to encrypt and decrypt a subset of session data on-the-fly preventing someone with access to the store from seeing the plain access tokens. They will only see an encrypted blob of text.
The way createEncryptedSessionStore
works is that it replaces the read and write functions of an express-session
compatible store with augmented versions that decrypt and encrypt a set of specified properties (if present on the session object) respectively. This means that you can still use any of the compatible store connectors and simply pass it through the helper function to get a version that provides encryption.
Using the Redis connector you can configure an encrypted session like this:
const session = require("express-session")
const { createEncryptedSessionStore } = require("@veracity/node-auth")
const redisStore = require("connect-redis")(session)
// You should NOT hard-code the encryption key. It should be served from a secure store such as Azure KeyVault or similar
const encryptedRedisStore = createEncryptedSessionStore("encryption key")(redisStore)
// We can now use the encryptedRedisStore in place of a regular store to configure authentication
setupWebAppAuth({
app,
strategy: {
clientId: "",
clientSecret: "",
replyUrl: ""
},
session: {
secret: "ce4dd9d9-cac3-4728-a7d7-d3e6157a06d9",
store: encryptedRedisStore // Use encrypted version of redis store
}
})
A helper to generate certificate that can be used for local development.
const express = require("express")
const app = express()
const https = require("https")
const { generateCertificate } = require("@veracity/node-auth")
app.get("/", (req, res, next) => {
res.send("Frontpage here")
})
// Set up the HTTPS development server
const server = https.createServer({
...generateCertificate() // Generate self-signed certificates for development
}, app)
server.on("error", (error) => { // If an error occurs halt the application
console.error(error)
process.exit(1)
})
server.listen(3000, () => { // Begin listening for connections
console.log("Listening for connections on https://localhost:3000/")
})