diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c63c08..1c4a358 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,2 +1,5 @@ { + "cSpell.words": [ + "dtos" + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 800e598..8ade33d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ This is the changelog for [Authress SDK](readme.md). * Add EdDSA support for `tokenVerifier()` class * Set the service client authorization request type to be `oauth-authz-req+jwt` * Handle malformed baseUrls in `httpClient`. +* Allow specifying the authress custom domain for service client machine to machine authentication. +* Add `users.getUser(userId)` api method. +* Add `connections` API to the SDK. ## 1.1 ## * Migrated to Github Actions diff --git a/README.md b/README.md index f07bd97..d09419c 100644 --- a/README.md +++ b/README.md @@ -30,14 +30,14 @@ const authressClient = new AuthressClient({ baseUrl: 'https://DOMAIN.api-REGION. // on api route [route('/resources/')] -function getResource(resourceId) { +async function getResource(resourceId) { // Get the user token and pass it to authress const authorizationToken = request.headers.get('authorization'); authressClient.setToken(authorizationToken); // Check Authress to authorize the user try { - authressClient.userPermissions.authorizeUser(userId, `resources/${resourceId}`, 'READ'); + await authressClient.userPermissions.authorizeUser(userId, `resources/${resourceId}`, 'READ'); } catch (error) { // Will throw except if the user is not authorized to read the resource if (error.status === 404) { @@ -64,10 +64,10 @@ const authressClient = new AuthressClient({ baseUrl: 'https://DOMAIN.api-REGION. // on api route [route('/resources/')] -function getResource(resourceId) { +async function getResource(resourceId) { // Check Authress to authorize the user try { - authressClient.userPermissions.authorizeUser(userId, `resources/${resourceId}`, 'READ'); + await authressClient.userPermissions.authorizeUser(userId, `resources/${resourceId}`, 'READ'); } catch (error) { // Will throw except if the user is not authorized to read the resource if (error.status === 404) { @@ -113,3 +113,31 @@ try { return { statusCode: 401 }; } ``` + +#### Make direct API requests +Authress supports extended functionality via the REST api, in specific cases it helps to make these direct calls. Each API call requires a URL and an access token. In the case you want use the access token for the user, directly pass it as the `bearer` in the `Authorization` header: +```js +const response = await client.get(url, { 'Authorization': `Bearer: ${userAccessToken}` }); +``` + +In the case you want to make a request using the service client's secret key, use the `serviceClientTokenProvider` you've already configured: +```js +// Standard library configuration: +const { AuthressClient, ServiceClientTokenProvider } = require('authress-sdk'); +const accessToken = 'eyJrZXlJ....'; +const serviceClientTokenProvider = new ServiceClientTokenProvider(accessToken); +const authressClient = new AuthressClient({ baseUrl: 'https://DOMAIN.api-REGION.authress.io' }, serviceClientTokenProvider); + +// Get a temporary token and use it: +const temporaryServiceClientAccessToken = await serviceClientTokenProvider.getToken(); +const response = await client.get(url, { 'Authorization': `Bearer: ${temporaryServiceClientAccessToken}` }); +``` + +## Contributions + +### Adding new DTO and methods +Auto generate the new code using this openapi generator, and merge the files into the appropriate locations: +```bash +curl -XPOST https://generator3.swagger.io/api/generate -H 'content-type: application/json' -d '{"specURL" : "https://api.authress.io/.well-known/openapi.json","lang" : "typescript-fetch","type" : "CLIENT","codegenVersion" : "V3"}' --output generated_sdk.tar.gz + +``` diff --git a/index.d.ts b/index.d.ts index d0ab69b..5fd79d3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,20 +1,20 @@ +/* eslint-disable node/no-missing-import */ /* eslint-disable @typescript-eslint/no-empty-interface */ /* eslint-disable no-shadow */ -export interface AuthressSettings { - //** Authress baseUrl => API Host: https://authress.io/app/#/api?route=overview */ - baseUrl: string; -} +import { Response } from './src/response'; -export interface Response { - /** Response data object on successful request */ - data: ResponseType; +import { ConnectionsApi } from './src/connections/api'; +export * from './src/connections/api'; +export * from './src/connections/dtos'; - /** Response headers */ - headers: Record; +import { TenantsApi } from './src/tenants/api'; +export * from './src/tenants/api'; +export * from './src/tenants/dtos'; - /** HTTP response status code for success responses */ - status: number; +export interface AuthressSettings { + //** Authress baseUrl => API Host: https://authress.io/app/#/api?route=overview */ + baseUrl: string; } /** @@ -1185,35 +1185,6 @@ export interface RolesApi { createRole(body: Role): Promise>; } -/** - * The user credentials for this connection which can be used to access the connection provider APIs. - * @export - * @interface UserConnectionCredentials - */ -export interface UserConnectionCredentials { - /** - * The access token. - * @type {string} - * @memberof UserConnectionCredentials - */ - accessToken: string; -} - -/** - * ConnectionsApi - * @export - */ -export interface ConnectionsApi { - /** - * Get the credentials for the user that were generated as part of the latest user login flow. Returns an access token that can be used with originating connection provider, based on the original scopes and approved permissions by that service. - * @summary Get the user credentials for this connection. - * @param {string} connectionId The connection to get the stored credentials. - * @param {string} [userId] The user to get the stored credentials, if not specified will automatically be populated by the token specified in the request to Authress. - * @throws {ArgumentRequiredError} - */ - getConnectionCredentials(connectionId: string, userId?: string): Promise>; -} - /** * ServiceClientsApi * @export @@ -1336,6 +1307,20 @@ export interface UserPermissionsApi { requestUserToken(userId?: string, body: TokenRequest): Promise>; } +/** + * UsersApi + * @export + */ +export interface UsersApi { + /** + * Get an Authress user + * @summary Retrieve a user with user data. + * @param {string} [userId] The user te get. + * @throws {ArgumentRequiredError} + */ + getUser(userId: string): Promise>; +} + /** * AuthressClient * @export @@ -1369,6 +1354,12 @@ export class AuthressClient { */ userPermissions: UserPermissionsApi; + /** + * @summary The Users api + * @type {UsersApi} + */ + users: UsersApi; + /** * @summary The Resources api * @type {ResourcesApi} @@ -1393,6 +1384,12 @@ export class AuthressClient { */ connections: ConnectionsApi; + /** + * @summary The Tenants api + * @type {TenantsApi} + */ + tenants: TenantsApi; + /** * @summary Set the users token here, so that requests made with this Authress Client will have the user's permissions * @type {Function} @@ -1411,9 +1408,10 @@ export class ServiceClientTokenProvider { * @constructor * @summary Create an instance of the service client token provider. Used to call the Authress API, when the user's token does not contain the necessary permissions. * @param {string} accessKey The service client access key, can be generated from https://authress.io/app/#/manage?focus=clients + * @param {string} authressCustomDomain The custom domain specified in your account under domain settings. What should my url be? => https://authress.io/app/#/setup?focus=domain */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - constructor(accessKey: string); + constructor(accessKey: string, authressCustomDomain: string); /** * @summary Generate a token from this token provider. In most cases should only be used by this library itself diff --git a/index.js b/index.js index a2d5c26..95d9d66 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,13 @@ const httpClient = require('./src/httpClient'); const AccessRecordsApi = require('./src/accessRecordsApi'); const UserPermissionsApi = require('./src/userPermissionsApi'); +const UsersApi = require('./src/usersApi'); const ServiceClientsApi = require('./src/serviceClientsApi'); const ResourcesApi = require('./src/resourcesApi'); const AccountsApi = require('./src/accountsApi'); const RolesApi = require('./src/rolesApi'); const ConnectionsApi = require('./src/connectionsApi'); +const TenantsApi = require('./src/tenantsApi'); class AuthressClient { constructor(settings, tokenProvider) { @@ -16,10 +18,12 @@ class AuthressClient { this.accessRecords = new AccessRecordsApi(this.httpClient); this.serviceClients = new ServiceClientsApi(this.httpClient); this.userPermissions = new UserPermissionsApi(this.httpClient); + this.users = new UsersApi(this.httpClient); this.resources = new ResourcesApi(this.httpClient); this.accounts = new AccountsApi(this.httpClient); this.roles = new RolesApi(this.httpClient); this.connections = new ConnectionsApi(this.httpClient); + this.tenants = new TenantsApi(this.httpClient); } setToken(token) { diff --git a/package.json b/package.json index 065516b..c8362ef 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,12 @@ "check-dts": "^0.4.4", "ci-build-tools": "^1.0.13", "commander": "^4.0.1", - "eslint": "^8.3.0", + "eslint": "^8.14.0", "eslint-config-cimpress-atsquad": "^1.0.67", "eslint-plugin-import": "^2.20.2", "eslint-plugin-mocha": "^7.0.1", "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.0.0", "fs-extra": "^8.1.0", "glob": "^7.1.6", "mocha": "^9.2.0", diff --git a/src/connections/api.ts b/src/connections/api.ts new file mode 100644 index 0000000..3ec9e92 --- /dev/null +++ b/src/connections/api.ts @@ -0,0 +1,58 @@ +/* eslint-disable node/no-missing-import */ +import { Response } from '../response'; +import { Connection, UserConnectionCredentials, ConnectionCollection } from './dtos'; + +/** + * ConnectionsApi + * @export + */ +export interface ConnectionsApi { + /** + * Specify identity connection details for Authress identity aggregation. + * @summary Create SSO connection + * @param {Connection} body + * @throws {RequiredError} + */ + createConnection(body: Connection): Promise>; + + /** + * Delete an identity connection details for Authress identity aggregation. + * @summary Delete SSO connection + * @param {string} connectionId The connection identifier. + * @throws {RequiredError} + */ + deleteConnection(connectionId: string): Promise>; + + /** + * Specify identity connection details for Authress identity aggregation. + * @summary Update SSO connection + * @param {Connection} body + * @param {string} connectionId The connection identifier. + * @throws {RequiredError} + */ + updateConnection(connectionId: string, body: Connection): Promise>; + + /** + * Get the identity connection details for Authress identity aggregation. + * @summary Retrieve SSO connection + * @param {string} connectionId The connection identifier. + * @throws {RequiredError} + */ + getConnection(connectionId: string): Promise>; + + /** + * Returns a paginated connection list for the account. Only connections the user has access to are returned. + * @summary List SSO connections + * @throws {RequiredError} + */ + getConnections(): Promise>; + + /** + * Get the credentials for the user that were generated as part of the latest user login flow. Returns an access token that can be used with originating connection provider, based on the original scopes and approved permissions by that service. + * @summary Get the user credentials for this connection. + * @param {string} connectionId The connection to get the stored credentials. + * @param {string} [userId] The user to get the stored credentials, if not specified will automatically be populated by the token specified in the request to Authress. + * @throws {ArgumentRequiredError} + */ + getConnectionCredentials(connectionId: string, userId?: string): Promise>; +} diff --git a/src/connections/dtos.ts b/src/connections/dtos.ts new file mode 100644 index 0000000..d9a3535 --- /dev/null +++ b/src/connections/dtos.ts @@ -0,0 +1,171 @@ +/* eslint-disable no-shadow */ +/* eslint-disable no-redeclare */ +/* eslint-disable @typescript-eslint/no-namespace */ +/** + * + * @export + * @interface Connection + */ +export interface Connection { + /** + * + * @type {string} + * @memberof Connection + */ + type?: Connection.TypeEnum; + /** + * + * @type {string} + * @memberof Connection + */ + connectionId?: string; + /** + * + * @type {string} + * @memberof Connection + */ + authenticationUrl: string; + /** + * + * @type {string} + * @memberof Connection + */ + tokenUrl?: string; + /** + * + * @type {string} + * @memberof Connection + */ + issuerUrl?: string; + /** + * + * @type {string} + * @memberof Connection + */ + providerCertificate?: string; + /** + * + * @type {string} + * @memberof Connection + */ + clientId?: string; + /** + * + * @type {string} + * @memberof Connection + */ + clientSecret?: string; + /** + * + * @type {ConnectionData} + * @memberof Connection + */ + data?: ConnectionData; + /** + * + * @type {ConnectionDefaultConnectionProperties} + * @memberof Connection + */ + defaultConnectionProperties?: ConnectionDefaultConnectionProperties; + /** + * + * @type {Date} + * @memberof Connection + */ + createdTime?: Date; +} + +/** + * @export + * @namespace Connection + */ +export namespace Connection { + /** + * @export + * @enum {string} + */ + export enum TypeEnum { + OAUTH2 = 'OAUTH2', + SAML2 = 'SAML2' + } +} +/** + * A collection of connections. + * @export + * @interface ConnectionCollection + */ +export interface ConnectionCollection { + /** + * + * @type {Array} + * @memberof ConnectionCollection + */ + connections: Array; +} +/** + * + * @export + * @interface ConnectionData + */ +export interface ConnectionData { + /** + * + * @type {string} + * @memberof ConnectionData + */ + tenantId?: string; + /** + * + * @type {string} + * @memberof ConnectionData + */ + name?: string; + /** + * + * @type {string} + * @memberof ConnectionData + */ + supportedContentType?: ConnectionData.SupportedContentTypeEnum; +} + +/** + * @export + * @namespace ConnectionData + */ +export namespace ConnectionData { + /** + * @export + * @enum {string} + */ + export enum SupportedContentTypeEnum { + Json = 'application/json', + XWwwFormUrlencoded = 'application/x-www-form-urlencoded' + } +} +/** + * + * @export + * @interface ConnectionDefaultConnectionProperties + */ +export interface ConnectionDefaultConnectionProperties { + /** + * + * @type {string} + * @memberof ConnectionDefaultConnectionProperties + */ + scope?: string; +} + +/** + * The user credentials for this connection which can be used to access the connection provider APIs. + * @export + * @interface UserConnectionCredentials + */ +export interface UserConnectionCredentials { + /** + * The access token. + * @type {string} + * @memberof UserConnectionCredentials + */ + accessToken: string; +} diff --git a/src/connectionsApi.js b/src/connectionsApi.js index 8a8317f..1af575b 100644 --- a/src/connectionsApi.js +++ b/src/connectionsApi.js @@ -12,9 +12,54 @@ class ConnectionsApi { this.client = client; } + async createConnection(connection) { + if (!connection) { + throw new ArgumentRequiredError('connection', 'Required parameter connection was not specified when calling createConnection.'); + } + + const response = await this.client.post('/v1/connections', connection); + return response; + } + + async deleteConnection(connectionId) { + if (!connectionId) { + throw new ArgumentRequiredError('connectionId', 'Required parameter connectionId was not specified when calling deleteConnection.'); + } + + const response = await this.client.delete(`/v1/connections/${encodeURIComponent(String(connectionId))}`); + return response; + } + + async getConnection(connectionId) { + if (!connectionId) { + throw new ArgumentRequiredError('connectionId', 'Required parameter connectionId was not specified when calling getConnection.'); + } + + const response = await this.client.get(`/v1/connections/${encodeURIComponent(String(connectionId))}`); + return response; + } + + async getConnections() { + const response = await this.client.get('/v1/connections/'); + return response; + } + + async updateConnection(connectionId, connection) { + if (!connectionId) { + throw new ArgumentRequiredError('connectionId', 'Required parameter connectionId was not specified when calling updateConnection.'); + } + + if (!connection) { + throw new ArgumentRequiredError('connection', 'Required parameter connection was not specified when calling updateConnection.'); + } + + const response = await this.client.put(`/v1/connections/${encodeURIComponent(String(connectionId))}`, connection); + return response; + } + async getConnectionCredentials(connectionId, userId) { if (!connectionId) { - throw new ArgumentRequiredError('connectionId', 'Required parameter connectionId was not specified when calling getRole.'); + throw new ArgumentRequiredError('connectionId', 'Required parameter connectionId was not specified when calling getConnectionCredentials.'); } const requestUserId = userId || await getFallbackUser(this.client); diff --git a/src/response.ts b/src/response.ts new file mode 100644 index 0000000..5fdb465 --- /dev/null +++ b/src/response.ts @@ -0,0 +1,10 @@ +export interface Response { + /** Response data object on successful request */ + data: ResponseType; + + /** Response headers */ + headers: Record; + + /** HTTP response status code for success responses */ + status: number; +} diff --git a/src/serviceClientTokenProvider.js b/src/serviceClientTokenProvider.js index ce686a6..7a28694 100644 --- a/src/serviceClientTokenProvider.js +++ b/src/serviceClientTokenProvider.js @@ -2,13 +2,13 @@ const { default: JwtSigner } = require('jose/jwt/sign'); const { createPrivateKey } = require('crypto'); const ArgumentRequiredError = require('./argumentRequiredError'); -module.exports = function(accessKey) { +module.exports = function(accessKey, authressCustomDomain) { const decodedAccessKey = { clientId: accessKey.split('.')[0], keyId: accessKey.split('.')[1], audience: `${accessKey.split('.')[2]}.accounts.authress.io`, privateKey: accessKey.split('.')[3] }; - const issuer = `https://api.authress.io/v1/clients/${encodeURIComponent(decodedAccessKey.clientId)}`; + const issuer = `https://${authressCustomDomain || 'api.authress.io'}/v1/clients/${encodeURIComponent(decodedAccessKey.clientId)}`; const innerGetToken = async () => { if (this.cachedKeyData && this.cachedKeyData.token && this.cachedKeyData.expires > Date.now() + 3600000) { diff --git a/src/tenants/api.ts b/src/tenants/api.ts new file mode 100644 index 0000000..c36d302 --- /dev/null +++ b/src/tenants/api.ts @@ -0,0 +1,49 @@ +/* eslint-disable node/no-missing-import */ +import { Response } from '../response'; +import { Tenant, TenantCollection } from './dtos'; + +/** + * TenantsApi + * @export + */ +export interface TenantsApi { + /** + * Specify identity tenant details for Authress identity aggregation. + * @summary Create SSO tenant + * @param {Tenant} body + * @throws {RequiredError} + */ + createTenant(body: Tenant): Promise>; + + /** + * Delete an identity tenant details for Authress identity aggregation. + * @summary Delete SSO tenant + * @param {string} tenantId The tenant identifier. + * @throws {RequiredError} + */ + deleteTenant(tenantId: string): Promise>; + + /** + * Specify identity tenant details for Authress identity aggregation. + * @summary Update SSO tenant + * @param {Tenant} body + * @param {string} tenantId The tenant identifier. + * @throws {RequiredError} + */ + updateTenant(tenantId: string, body: Tenant): Promise>; + + /** + * Get the identity tenant details for Authress identity aggregation. + * @summary Retrieve SSO tenant + * @param {string} tenantId The tenant identifier. + * @throws {RequiredError} + */ + getTenant(tenantId: string): Promise>; + + /** + * Returns a paginated tenant list for the account. Only tenants the user has access to are returned. + * @summary List SSO tenants + * @throws {RequiredError} + */ + getTenants(): Promise>; +} diff --git a/src/tenants/dtos.ts b/src/tenants/dtos.ts new file mode 100644 index 0000000..d42c801 --- /dev/null +++ b/src/tenants/dtos.ts @@ -0,0 +1,79 @@ +/* eslint-disable no-shadow */ +/* eslint-disable no-redeclare */ +/* eslint-disable @typescript-eslint/no-namespace */ +/** + * + * @export + * @interface Tenant + */ +export interface Tenant { + /** + * + * @type {string} + * @memberof Tenant + */ + tenantId: string; + /** + * + * @type {string} + * @memberof Tenant + */ + tenantLookupIdentifier?: string; + /** + * + * @type {TenantData} + * @memberof Tenant + */ + data?: TenantData; + /** + * + * @type {TenantConnection} + * @memberof Tenant + */ + connection?: TenantConnection; + /** + * + * @type {Date} + * @memberof Tenant + */ + createdTime?: Date; +} +/** +* A collection of tenants. +* @export +* @interface TenantCollection +*/ +export interface TenantCollection { + /** + * + * @type {Array} + * @memberof TenantCollection + */ + connections?: Array; +} +/** +* +* @export +* @interface TenantConnection +*/ +export interface TenantConnection { + /** + * + * @type {string} + * @memberof TenantConnection + */ + connectionId?: string; +} +/** +* +* @export +* @interface TenantData +*/ +export interface TenantData { + /** + * + * @type {string} + * @memberof TenantData + */ + name?: string; +} diff --git a/src/tenantsApi.js b/src/tenantsApi.js new file mode 100644 index 0000000..b1722b1 --- /dev/null +++ b/src/tenantsApi.js @@ -0,0 +1,54 @@ +const ArgumentRequiredError = require('./argumentRequiredError'); + +class TenantsApi { + constructor(client) { + this.client = client; + } + + async createTenant(tenant) { + if (!tenant) { + throw new ArgumentRequiredError('tenant', 'Required parameter tenant was not specified when calling createTenant.'); + } + + const response = await this.client.post('/v1/tenants', tenant); + return response; + } + + async deleteTenant(tenantId) { + if (!tenantId) { + throw new ArgumentRequiredError('tenantId', 'Required parameter tenantId was not specified when calling deleteTenant.'); + } + + const response = await this.client.delete(`/v1/tenants/${encodeURIComponent(String(tenantId))}`); + return response; + } + + async getTenant(tenantId) { + if (!tenantId) { + throw new ArgumentRequiredError('tenantId', 'Required parameter tenantId was not specified when calling getTenant.'); + } + + const response = await this.client.get(`/v1/tenants/${encodeURIComponent(String(tenantId))}`); + return response; + } + + async getTenants() { + const response = await this.client.get('/v1/tenants'); + return response; + } + + async updateTenant(tenantId, tenant) { + if (!tenantId) { + throw new ArgumentRequiredError('tenantId', 'Required parameter tenantId was not specified when calling updateTenant.'); + } + + if (!tenant) { + throw new ArgumentRequiredError('tenant', 'Required parameter tenant was not specified when calling updateTenant.'); + } + + const response = await this.client.put(`/v1/tenants/${encodeURIComponent(String(tenantId))}`, tenant); + return response; + } +} + +module.exports = TenantsApi; diff --git a/src/tokenVerifier.js b/src/tokenVerifier.js index 7fed2b4..5773d93 100644 --- a/src/tokenVerifier.js +++ b/src/tokenVerifier.js @@ -47,7 +47,7 @@ async function getPublicKey(jwkKeyListUrl, kid) { } } -module.exports = async function(authressCustomDomain, requestToken, options = { verifierOptions: {} }) { +module.exports = async function(authressCustomDomain, requestToken, options = { expectedPublicKey: {}, verifierOptions: {} }) { if (!requestToken) { const error = new Error('Unauthorized'); error.code = 'Unauthorized'; @@ -83,13 +83,22 @@ module.exports = async function(authressCustomDomain, requestToken, options = { throw error; } - if (issuer !== new URL(`https://${authressCustomDomain.replace(/^(https?:\/\/)/, '')}`).origin) { + const completeIssuerUrl = new URL(`https://${authressCustomDomain.replace(/^(https?:\/\/)/, '')}`); + if (new URL(issuer).origin !== completeIssuerUrl.origin) { const error = new Error(`Unauthorized: Invalid Issuer: ${issuer}`); error.code = 'Unauthorized'; throw error; } - const key = await getPublicKey(`${issuer}/.well-known/openid-configuration/jwks`, kid); + // Handle service client checking + const clientIdMatcher = completeIssuerUrl.pathname.match(/^\/v\d\/clients\/([^/]+)$/); + if (clientIdMatcher && clientIdMatcher[1] !== unverifiedToken.payload.sub) { + const error = new Error(`Unauthorized: Invalid Sub found for service client token: ${unverifiedToken.payload.sub}`); + error.code = 'Unauthorized'; + throw error; + } + + const key = options.expectedPublicKey || await getPublicKey(`${issuer}/.well-known/openid-configuration/jwks`, kid); try { const verifiedToken = await verifyJwt(authenticationToken, await parseJwk(key), { algorithms: ['EdDSA', 'RS512'], issuer, ...options.verifierOptions }); diff --git a/src/usersApi.js b/src/usersApi.js new file mode 100644 index 0000000..6b5b3a5 --- /dev/null +++ b/src/usersApi.js @@ -0,0 +1,19 @@ +const ArgumentRequiredError = require('./argumentRequiredError'); + +class UsersApi { + constructor(client) { + this.client = client; + } + + async getUser(userId) { + // verify required parameter 'userId' is not null or undefined + if (userId === null || userId === undefined) { + throw new ArgumentRequiredError('userId', 'Required parameter userId was null or undefined when calling getUser.'); + } + const url = `/v1/users/${encodeURIComponent(String(userId))}`; + const response = await this.client.get(url); + return response; + } +} + +module.exports = UsersApi; diff --git a/tests/tokenVerifier.test.js b/tests/tokenVerifier.test.js index 5fc79d7..6fd6dbd 100644 --- a/tests/tokenVerifier.test.js +++ b/tests/tokenVerifier.test.js @@ -1,5 +1,6 @@ const { describe, it, beforeEach, afterEach } = require('mocha'); const sinon = require('sinon'); +const { ServiceClientTokenProvider } = require('..'); const { TokenVerifier } = require('../index'); @@ -7,6 +8,8 @@ let sandbox; beforeEach(() => { sandbox = sinon.createSandbox(); }); afterEach(() => sandbox.restore()); +const customDomain = 'authress.token-validation.test'; + describe('tokenVerifier.js', () => { describe('verifyToken', () => { it('Validate RS512 token works', async () => { @@ -14,9 +17,13 @@ describe('tokenVerifier.js', () => { await TokenVerifier('https://login.authress.io', userToken, { verifierOptions: { currentDate: new Date('2021-11-07') } }); }).timeout(10000); - it.skip('Validate EdDSA token works', async () => { - const userToken = 'eyJhbGciOiJFZERTQSIsImtpZCI6InBYUHR0VXdXSG9lYUVHeVdmRU5oV2YiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2F1dGhyZXNzLnRva2VuLXZhbGlkYXRpb24udGVzdCIsInN1YiI6InVzZXIxMTEiLCJpYXQiOjE2MzYyMDY0MzYsImV4cCI6MTYzNjI5MjgzNiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCIsImF6cCI6ImF1dGhvcml6YXRpb24tc291cmNlIiwiY2xpZW50X2lkIjoiYXV0aHJlc3Mtc2RrLmpzIiwiYXVkIjpbInRlbmFudCJdfQ.rOLSy95ZK80AktkzvAeuKQXbyHETYj2Ee5_zAJmAUpPJx0EISB3urR6RV74YJiov94jAZ7iPsR8fJ3OWiWtTDw'; - await TokenVerifier('https://login.authress.io', userToken, { verifierOptions: { currentDate: new Date('2021-11-07') } }); + it('Validate EdDSA service client token', async () => { + const accessKey = 'sc_aAKceYi7VJDCU8vB7HcTo3Q.pogP.a43706ca-9647-40e4-aeae-7dcaa54bbab3.MC4CAQAwBQYDK2VwBCIEIDVjjrIVCH3dVRq4ixRzBwjVHSoB2QzZ2iJuHq1Wshwp'; + const publicKey = { alg: 'EdDSA', kty: 'OKP', crv: 'Ed25519', x: 'JxtSC5tZZJuaW7Aeu5Kh_3tgCpPZRkHaaFyTj5sQ3KU' }; + const tokenProvider = new ServiceClientTokenProvider(accessKey, customDomain); + const initialToken = await tokenProvider.getToken(); + + await TokenVerifier(`https://${customDomain}`, initialToken, { expectedPublicKey: publicKey, verifierOptions: { currentDate: new Date('2022-05-07') } }); }).timeout(10000); }); }); diff --git a/yarn.lock b/yarn.lock index 1717235..42ad61f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,6 +17,21 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@eslint/eslintrc@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.2.tgz#4989b9e8c0216747ee7cca314ae73791bb281aae" + integrity sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.3.1" + globals "^13.9.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + "@humanwhocodes/config-array@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.6.0.tgz#b5621fdb3b32309d2d16575456cbc277fa8f021a" @@ -26,7 +41,16 @@ debug "^4.1.1" minimatch "^3.0.4" -"@humanwhocodes/object-schema@^1.2.0": +"@humanwhocodes/config-array@^0.9.2": + version "0.9.5" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7" + integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0", "@humanwhocodes/object-schema@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== @@ -176,6 +200,11 @@ acorn@^8.6.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== +acorn@^8.7.0: + version "8.7.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -693,6 +722,11 @@ eslint-plugin-promise@>=4.0.0: resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-5.1.1.tgz#9674d11c056d1bafac38e4a3a9060be740988d90" integrity sha512-XgdcdyNzHfmlQyweOPTxmc7pIsS6dE4MvwhXWMQ2Dxs1XAL2GJDilUsjWen6TWik0aSI+zD/PqocZBblcm9rdA== +eslint-plugin-promise@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz#017652c07c9816413a41e11c30adc42c3d55ff18" + integrity sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw== + eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -709,6 +743,14 @@ eslint-scope@^7.1.0: esrecurse "^4.3.0" estraverse "^5.2.0" +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + eslint-utils@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" @@ -738,7 +780,12 @@ eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz#eee4acea891814cda67a7d8812d9647dd0179af2" integrity sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA== -eslint@>=5.0.0, eslint@^8.3.0: +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@>=5.0.0: version "8.3.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.3.0.tgz#a3c2409507403c1c7f6c42926111d6cbefbc3e85" integrity sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww== @@ -782,6 +829,47 @@ eslint@>=5.0.0, eslint@^8.3.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +eslint@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.14.0.tgz#62741f159d9eb4a79695b28ec4989fcdec623239" + integrity sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw== + dependencies: + "@eslint/eslintrc" "^1.2.2" + "@humanwhocodes/config-array" "^0.9.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^6.0.1" + globals "^13.6.0" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + espree@^9.0.0, espree@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/espree/-/espree-9.1.0.tgz#ba9d3c9b34eeae205724124e31de4543d59fbf74" @@ -791,6 +879,15 @@ espree@^9.0.0, espree@^9.1.0: acorn-jsx "^5.3.1" eslint-visitor-keys "^3.1.0" +espree@^9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" + integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== + dependencies: + acorn "^8.7.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^3.3.0" + esquery@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" @@ -1070,6 +1167,11 @@ ignore@^5.1.1, ignore@^5.1.4, ignore@^5.1.8: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.9.tgz#9ec1a5cbe8e1446ec60d4420060d43aa6e7382fb" integrity sha512-2zeMQpbKz5dhZ9IwL0gbxSW5w0NK/MSAMtNuhgIHEPmaU3vPdKPL0UdvUCXs5SS4JAwsBxysK5sFMW8ocFiVjQ== +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"