Skip to content

Commit 0ff393c

Browse files
committed
feat: add self managed signer and remove federated pool auth
1 parent c7ab930 commit 0ff393c

File tree

6 files changed

+153
-277
lines changed

6 files changed

+153
-277
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Add the dependency to your package.json and save it:
1212

1313
```
1414
"dependencies": {
15-
"@scribelabsai/auth": ">=1.0.0"
15+
"@scribelabsai/auth": ">=2.0.0"
1616
}
1717
```
1818

bin/auth.ts

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,79 @@
11
#! /usr/bin/env node
22

3-
import { Auth } from '@scribelabsai/auth';
4-
import { program } from 'commander';
3+
import { input, password as passwordPrompt } from '@inquirer/prompts';
4+
import { Option, program } from 'commander';
5+
import 'dotenv/config';
6+
import { authenticator } from 'otplib';
7+
import { Auth } from '../src/index.js';
58

69
program
710
.command('token')
811
.description('Get all the JWTs (id, access and refresh) given a user/password pair')
9-
.arguments('<clientid> <userpoolid> <username> <password>')
10-
.action(async (clientid: string, userpoolid: string, username: string, password: string) => {
11-
const auth = new Auth({ clientId: clientid, userPoolId: userpoolid });
12-
const tokens = await auth.getTokens({ username, password });
13-
console.info(tokens);
12+
.addOption(
13+
new Option('-c, --clientid <clientid>', 'Cognito Client ID')
14+
.env('CLIENT_ID')
15+
.makeOptionMandatory(true)
16+
)
17+
.addOption(
18+
new Option('-u, --userpoolid <userpoolid>', 'Cognito User Pool ID')
19+
.env('USER_POOL_ID')
20+
.makeOptionMandatory(true)
21+
)
22+
.addOption(new Option('-n, --username <username>', 'Username'))
23+
.addOption(new Option('-p, --password <password>', 'Password'))
24+
.action(async (options) => {
25+
const auth = new Auth({
26+
clientId: options.clientid,
27+
userPoolId: options.userpoolid,
28+
});
29+
30+
// Prompt for username if not provided
31+
const username =
32+
options.username ||
33+
(await input({
34+
message: 'Enter username:',
35+
}));
36+
37+
// Prompt for password if not provided
38+
const password =
39+
options.password ||
40+
(await passwordPrompt({
41+
message: 'Enter password:',
42+
mask: true,
43+
}));
44+
45+
const result = await auth.getTokens({
46+
username,
47+
password,
48+
});
49+
50+
// Check if result is a Challenge (MFA required)
51+
if ('challengeName' in result && 'challengeParameters' in result && 'user' in result) {
52+
const mfaCode = await input({
53+
message: 'Enter MFA code:',
54+
});
55+
56+
const tokens = await auth.respondToAuthChallengeMfa(
57+
result.user,
58+
mfaCode,
59+
result.challengeParameters
60+
);
61+
console.info(tokens);
62+
} else {
63+
// Result is already Tokens
64+
console.info(result);
65+
}
1466
});
1567

1668
program
17-
.command('credentials')
18-
.description('Get the credentials using an id token')
19-
.arguments('<clientid> <userpoolid> <fedid> <token>')
20-
.action(async (clientid: string, userpoolid: string, fedid: string, token: string) => {
21-
const auth = new Auth({ clientId: clientid, userPoolId: userpoolid, identityPoolId: fedid });
22-
const fedUserId = await auth.getFederatedId(token);
23-
const credentials = await auth.getFederatedCredentials(fedUserId, token);
24-
console.info(credentials);
69+
.command('otp')
70+
.description('Generate an OTP code given a secret')
71+
.addOption(
72+
new Option('-s, --secret <secret>', 'OTP Secret').env('OTPCODE').makeOptionMandatory(true)
73+
)
74+
.action((options) => {
75+
const otpCode = authenticator.generate(options.secret);
76+
console.info('OTP Code:', otpCode);
2577
});
2678

2779
program.parse(process.argv);

package.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,8 @@
5353
},
5454
"dependencies": {
5555
"@aws-sdk/client-cognito-identity": "^3.348.0",
56-
"@aws-sdk/client-cognito-identity-provider": "^3.332.0",
57-
"@smithy/protocol-http": "^3.0.8",
58-
"@smithy/signature-v4": "^2.0.1",
56+
"@aws-sdk/client-cognito-identity-provider": "^3.760.0",
5957
"amazon-cognito-identity-js": "^6.2.0",
60-
"aws-sdk": "^2.1379.0",
61-
"commander": "^11.0.0"
58+
"jsonwebtoken": "^9.0.2"
6259
}
6360
}

src/auth.ts

Lines changed: 45 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,20 @@
1-
import { Sha256 } from '@aws-crypto/sha256-js';
2-
import {
3-
CognitoIdentityClient,
4-
GetCredentialsForIdentityCommand,
5-
GetCredentialsForIdentityCommandOutput,
6-
GetIdCommand,
7-
} from '@aws-sdk/client-cognito-identity';
81
import {
92
CognitoIdentityProvider,
103
InitiateAuthCommandOutput,
114
} from '@aws-sdk/client-cognito-identity-provider';
12-
import { HttpRequest } from '@smithy/protocol-http';
13-
import { SignatureV4 } from '@smithy/signature-v4';
145
import {
156
AuthenticationDetails,
167
ClientMetadata,
178
CognitoUser,
189
CognitoUserPool,
1910
} from 'amazon-cognito-identity-js';
11+
import jwt from 'jsonwebtoken';
2012
import {
2113
MFAError,
2214
MissingFieldError,
2315
MissingIdError,
2416
TooManyRequestsError,
2517
UnauthorizedError,
26-
UnknownError,
2718
} from './errors.js';
2819

2920
export interface UsernamePassword {
@@ -56,39 +47,69 @@ export interface Credentials {
5647
Expiration: Date;
5748
}
5849

59-
function isCompleteCredentials(
60-
cred: GetCredentialsForIdentityCommandOutput['Credentials']
61-
): cred is Credentials {
62-
return !!cred?.AccessKeyId && !!cred.SecretKey && !!cred.SessionToken && !!cred.Expiration;
50+
export class SelfManagedSigner {
51+
/**
52+
* Creates a signer for Self-Managed JWT auth.
53+
* @param privateKey - Private key related to the public key provided to Scribe.
54+
* @param issuer - Issuer as communicated to Scribe. Usually the company name.
55+
* @param sub - Account id. Provided by Scribe.
56+
*/
57+
constructor(
58+
private privateKey: string,
59+
private issuer: string,
60+
private sub: string
61+
) {}
62+
63+
/**
64+
* Signs a JWT with the private key.
65+
* @param scopes - The scopes to include in the JWT.
66+
* @param exp - The expiration time of the JWT in seconds.
67+
* @returns The signed JWT.
68+
*/
69+
sign(scopes: string[], exp: number): string {
70+
const payload = {
71+
iss: this.issuer,
72+
sub: this.sub,
73+
aud: 'https://apis.scribelabs.ai',
74+
scope: scopes.join(' '),
75+
exp: exp,
76+
};
77+
78+
return jwt.sign(payload, this.privateKey, { algorithm: 'RS256' });
79+
}
80+
}
81+
82+
/**
83+
* Decodes a JWT.
84+
* @param token - The JWT to decode.
85+
* @param publicKey - The public key to verify the JWT with.
86+
* @returns The decoded JWT payload.
87+
*/
88+
export function decodeSelfSignedJwt(token: string, publicKey: string): jwt.JwtPayload {
89+
return jwt.verify(token, publicKey, {
90+
algorithms: ['RS256'],
91+
audience: 'https://apis.scribelabs.ai',
92+
}) as jwt.JwtPayload;
6393
}
6494

6595
export class Auth {
6696
private client: CognitoIdentityProvider;
67-
private fedClient: CognitoIdentityClient | undefined;
6897
private clientId: string;
6998
private userPoolId: string;
70-
private identityPoolId: string | undefined;
7199

72100
/**
73101
* Construct an authorization client.
74102
* @param params - The parameters to construct the client.
75103
* @param params.clientId - The client ID of the application provided by Scribe.
76104
* @param params.userPoolId - The user pool ID provided by Scribe.
77-
* @param params.identityPoolId - The identity pool ID provided by Scribe.
78105
*/
79-
constructor(params: { clientId: string; userPoolId: string; identityPoolId?: string }) {
106+
constructor(params: { clientId: string; userPoolId: string }) {
80107
const region = 'eu-west-2';
81108
this.client = new CognitoIdentityProvider({
82109
region,
83110
});
84111
this.clientId = params.clientId;
85112
this.userPoolId = params.userPoolId;
86-
this.identityPoolId = params.identityPoolId;
87-
if (params.identityPoolId) {
88-
this.fedClient = new CognitoIdentityClient({
89-
region,
90-
});
91-
}
92113
}
93114

94115
/**
@@ -334,101 +355,6 @@ export class Auth {
334355
}
335356
}
336357

337-
async getFederatedId(idToken: string): Promise<string> {
338-
/**
339-
* A user gets their federated id.
340-
*
341-
* @param idToken - Id token to use.
342-
* @returns A string containing the federatedId.
343-
*/
344-
if (!this.userPoolId) throw new MissingIdError('Missing user pool ID');
345-
if (!this.fedClient)
346-
throw new MissingIdError(
347-
'Identity Pool ID is not provided. Create a new Auth object using identityPoolId'
348-
);
349-
try {
350-
const response = await this.fedClient.send(
351-
new GetIdCommand({
352-
IdentityPoolId: this.identityPoolId,
353-
Logins: {
354-
[`cognito-idp.eu-west-2.amazonaws.com/${this.userPoolId}`]: idToken,
355-
},
356-
})
357-
);
358-
if (!response.IdentityId) throw new UnknownError('Could not retrieve federated id');
359-
return response.IdentityId;
360-
} catch (err) {
361-
if (err instanceof Error && err.name === 'NotAuthorizedException')
362-
throw new UnauthorizedError('Could not retrieve federated id', err);
363-
else if (err instanceof Error && err.name === 'TooManyRequestsException')
364-
throw new TooManyRequestsError('Too many requests. Try again later');
365-
throw err;
366-
}
367-
}
368-
369-
async getFederatedCredentials(id: string, idToken: string): Promise<Credentials> {
370-
/**
371-
* A user gets their federated credentials (AccessKeyId, SecretKey and SessionToken).
372-
*
373-
* @param id - Federated id.
374-
* @param idToken - Id token to use.
375-
* @returns Credentials - Object containing the AccessKeyId, SecretKey, SessionToken and Expiration.
376-
* { "AccessKeyId": string, "SecretKey": string, "SessionToken": string, "Expiration": string }
377-
*/
378-
if (!this.userPoolId) throw new MissingIdError('Missing user pool ID');
379-
if (!this.fedClient)
380-
throw new MissingIdError(
381-
'Identity Pool ID is not provided. Create a new Auth object using identityPoolId'
382-
);
383-
try {
384-
const response = await this.fedClient.send(
385-
new GetCredentialsForIdentityCommand({
386-
IdentityId: id,
387-
Logins: {
388-
[`cognito-idp.eu-west-2.amazonaws.com/${this.userPoolId}`]: idToken,
389-
},
390-
})
391-
);
392-
if (!isCompleteCredentials(response.Credentials))
393-
throw new UnknownError('Could not retrieve federated credentials');
394-
return response.Credentials;
395-
} catch (err) {
396-
if (err instanceof Error && err.name === 'NotAuthorizedException')
397-
throw new UnauthorizedError('Could not retrieve federated credentials', err);
398-
else if (err instanceof Error && err.name === 'TooManyRequestsException')
399-
throw new TooManyRequestsError('Too many requests. Try again later');
400-
else if (err instanceof Error && err.name === 'ResourceNotFoundException')
401-
throw new UnauthorizedError('Federated id incorrect', err);
402-
throw err;
403-
}
404-
}
405-
406-
async getSignatureForRequest(request: HttpRequest, credentials: Credentials) {
407-
/**
408-
* A user gets a signature for a request.
409-
*
410-
* @param request - Request to send.
411-
* @param credentials - Credentials for the signature creation.
412-
* @returns HeaderBag - Headers containing the signature for the request.
413-
*/
414-
try {
415-
const signer = new SignatureV4({
416-
credentials: {
417-
accessKeyId: credentials.AccessKeyId,
418-
secretAccessKey: credentials.SecretKey,
419-
sessionToken: credentials.SessionToken,
420-
},
421-
service: 'execute-api',
422-
region: 'eu-west-2',
423-
sha256: Sha256,
424-
});
425-
const signatureRequest = await signer.sign(request);
426-
return signatureRequest.headers;
427-
} catch (err) {
428-
throw err;
429-
}
430-
}
431-
432358
// async revokeRefreshToken(refreshToken: string): Promise<boolean> {
433359
// /**
434360
// Revokes all of the access tokens generated by the specified refresh token.

0 commit comments

Comments
 (0)