Skip to content

Commit c364b4e

Browse files
committed
feat(api): add support for ClientCreds style cognito tokens
Before, only user creds were supported by the oauth2/token route.
1 parent 743caf5 commit c364b4e

File tree

5 files changed

+158
-33
lines changed

5 files changed

+158
-33
lines changed

src/__tests__/mockTokenGenerator.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ import { TokenGenerator } from "../services/tokenGenerator";
22

33
export const newMockTokenGenerator = (): jest.Mocked<TokenGenerator> => ({
44
generate: jest.fn(),
5+
generateWithClientCreds: jest.fn(),
56
});

src/server/server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,19 @@ export const createServer = (
6868
req.on("end", function () {
6969
const target = "GetToken";
7070
const route = router(target);
71+
const auth = req.get("Authorization");
72+
if (auth && auth.startsWith("Basic ")) {
73+
const sliced = auth.slice("Basic ".length);
74+
const buff = new Buffer(sliced, "base64");
75+
const decoded = buff.toString("ascii");
76+
const creds = decoded.split(":");
77+
if (creds.length == 2) {
78+
const id = creds[0];
79+
const secret = creds[1];
80+
rawBody += `&client_id=${id}`;
81+
rawBody += `&client_secret=${secret}`;
82+
}
83+
}
7184
route({ logger: req.log }, rawBody).then(
7285
(output) => {
7386
res.status(200).type("json").send(JSON.stringify(output));

src/services/tokenGenerator.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ const applyTokenOverrides = (
8686

8787
export interface Tokens {
8888
readonly AccessToken: string;
89-
readonly IdToken: string;
90-
readonly RefreshToken: string;
89+
readonly IdToken?: string;
90+
readonly RefreshToken?: string;
9191
}
9292

9393
export interface TokenGenerator {
@@ -104,6 +104,10 @@ export interface TokenGenerator {
104104
| "NewPasswordChallenge"
105105
| "RefreshTokens"
106106
): Promise<Tokens>;
107+
generateWithClientCreds(
108+
ctx: Context,
109+
userPoolClient: AppClient
110+
): Promise<Tokens>;
107111
}
108112

109113
const formatExpiration = (
@@ -240,4 +244,40 @@ export class JwtTokenGenerator implements TokenGenerator {
240244
),
241245
};
242246
}
247+
248+
public async generateWithClientCreds(
249+
ctx: Context,
250+
userPoolClient: AppClient
251+
): Promise<Tokens> {
252+
const eventId = uuid.v4();
253+
const authTime = Math.floor(this.clock.get().getTime() / 1000);
254+
255+
const accessToken: RawToken = {
256+
auth_time: authTime,
257+
client_id: userPoolClient.ClientId,
258+
event_id: eventId,
259+
iat: authTime,
260+
jti: uuid.v4(),
261+
scope: "aws.cognito.signin.user.admin", // TODO: scopes
262+
sub: userPoolClient.ClientId,
263+
token_use: "access",
264+
};
265+
266+
const issuer = `${this.tokenConfig.IssuerDomain}/${userPoolClient.UserPoolId}`;
267+
268+
return await Promise.resolve({
269+
IdToken: null,
270+
RefreshToken: null,
271+
AccessToken: jwt.sign(accessToken, PrivateKey.pem, {
272+
algorithm: "RS256",
273+
issuer,
274+
expiresIn: formatExpiration(
275+
userPoolClient.AccessTokenValidity,
276+
userPoolClient.TokenValidityUnits?.AccessToken ?? "hours",
277+
"24h"
278+
),
279+
keyid: "CognitoLocal",
280+
}),
281+
});
282+
}
243283
}

src/targets/getToken.test.ts

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1-
import {newMockCognitoService} from "../__tests__/mockCognitoService";
2-
import {newMockTokenGenerator} from "../__tests__/mockTokenGenerator";
3-
import {newMockTriggers} from "../__tests__/mockTriggers";
4-
import {newMockUserPoolService} from "../__tests__/mockUserPoolService";
5-
import {TestContext} from "../__tests__/testContext";
1+
import { newMockCognitoService } from "../__tests__/mockCognitoService";
2+
import { newMockTokenGenerator } from "../__tests__/mockTokenGenerator";
3+
import { newMockTriggers } from "../__tests__/mockTriggers";
4+
import { newMockUserPoolService } from "../__tests__/mockUserPoolService";
5+
import { TestContext } from "../__tests__/testContext";
66
import * as TDB from "../__tests__/testDataBuilder";
7-
import {CognitoService, Triggers, UserPoolService} from "../services";
8-
import {TokenGenerator} from "../services/tokenGenerator";
7+
import { CognitoService, Triggers, UserPoolService } from "../services";
8+
import { TokenGenerator } from "../services/tokenGenerator";
99

10-
import {
11-
GetToken,
12-
GetTokenTarget,
13-
} from "./getToken";
10+
import { GetToken, GetTokenTarget } from "./getToken";
1411

1512
describe("GetToken target", () => {
16-
let target: GetTokenTarget;
17-
13+
let getToken: GetTokenTarget;
1814
let mockCognitoService: jest.Mocked<CognitoService>;
1915
let mockTokenGenerator: jest.Mocked<TokenGenerator>;
2016
let mockTriggers: jest.Mocked<Triggers>;
@@ -23,42 +19,84 @@ describe("GetToken target", () => {
2319

2420
beforeEach(() => {
2521
mockUserPoolService = newMockUserPoolService({
26-
Id : userPoolClient.UserPoolId,
22+
Id: userPoolClient.UserPoolId,
2723
});
2824
mockCognitoService = newMockCognitoService(mockUserPoolService);
2925
mockCognitoService.getAppClient.mockResolvedValue(userPoolClient);
3026
mockTriggers = newMockTriggers();
3127
mockTokenGenerator = newMockTokenGenerator();
3228
getToken = GetToken({
33-
triggers : mockTriggers,
34-
cognito : mockCognitoService,
35-
tokenGenerator : mockTokenGenerator,
29+
cognito: mockCognitoService,
30+
tokenGenerator: mockTokenGenerator,
3631
});
3732
});
3833

3934
it("issues access tokens via refresh tokens", async () => {
4035
mockTokenGenerator.generate.mockResolvedValue({
41-
AccessToken : "access",
42-
IdToken : "id",
43-
RefreshToken : "refresh",
36+
AccessToken: "access",
37+
IdToken: "id",
38+
RefreshToken: "refresh",
4439
});
4540

4641
const existingUser = TDB.user({
47-
RefreshTokens : [ "refresh-orig" ],
42+
RefreshTokens: ["refresh-orig"],
4843
});
4944
mockUserPoolService.getUserByRefreshToken.mockResolvedValue(existingUser);
5045
mockUserPoolService.listUserGroupMembership.mockResolvedValue([]);
5146

5247
const response = await getToken(
53-
TestContext,
54-
new URLSearchParams(`client_id=${
55-
userPoolClient
56-
.ClientId}&grant_type=refresh_token&refresh_token=refresh-orig`));
57-
expect(mockUserPoolService.getUserByRefreshToken)
58-
.toHaveBeenCalledWith(TestContext, "refresh-orig");
48+
TestContext,
49+
new URLSearchParams(
50+
`client_id=${userPoolClient.ClientId}&grant_type=refresh_token&refresh_token=refresh-orig`
51+
)
52+
);
53+
expect(mockUserPoolService.getUserByRefreshToken).toHaveBeenCalledWith(
54+
TestContext,
55+
"refresh-orig"
56+
);
5957
expect(mockUserPoolService.storeRefreshToken).not.toHaveBeenCalled();
6058

6159
expect(response.access_token).toEqual("access");
6260
expect(response.refresh_token).toEqual("refresh");
6361
});
6462
});
63+
64+
describe("GetToken target - Client Creds", () => {
65+
let getToken: GetTokenTarget;
66+
let mockCognitoService: jest.Mocked<CognitoService>;
67+
let mockTokenGenerator: jest.Mocked<TokenGenerator>;
68+
let mockUserPoolService: jest.Mocked<UserPoolService>;
69+
const userPoolClient = TDB.appClient({
70+
ClientSecret: "secret",
71+
ClientId: "id",
72+
});
73+
74+
beforeEach(() => {
75+
mockUserPoolService = newMockUserPoolService({
76+
Id: userPoolClient.UserPoolId,
77+
});
78+
mockCognitoService = newMockCognitoService(mockUserPoolService);
79+
mockCognitoService.getAppClient.mockResolvedValue(userPoolClient);
80+
mockTokenGenerator = newMockTokenGenerator();
81+
getToken = GetToken({
82+
cognito: mockCognitoService,
83+
tokenGenerator: mockTokenGenerator,
84+
});
85+
});
86+
87+
it("issues access tokens via client credentials", async () => {
88+
mockTokenGenerator.generateWithClientCreds.mockResolvedValue({
89+
AccessToken: "access",
90+
RefreshToken: null,
91+
IdToken: null,
92+
});
93+
94+
const response = await getToken(
95+
TestContext,
96+
new URLSearchParams(
97+
`client_id=${userPoolClient.ClientId}&client_secret=${userPoolClient.ClientSecret}&grant_type=client_credentials`
98+
)
99+
);
100+
expect(response.access_token).toEqual("access");
101+
});
102+
});

src/targets/getToken.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ type GetTokenRequest = URLSearchParams;
1313

1414
interface GetTokenResponse {
1515
access_token: string;
16-
refresh_token: string;
16+
refresh_token?: string;
1717
}
1818

1919
export type GetTokenTarget = Target<GetTokenRequest, GetTokenResponse>;
2020

21-
async function getRefreshToken(
21+
async function getWithRefreshToken(
2222
ctx: Context,
2323
services: HandleTokenServices,
2424
params: GetTokenRequest
@@ -51,6 +51,38 @@ async function getRefreshToken(
5151
};
5252
}
5353

54+
async function getWithClientCredentials(
55+
ctx: Context,
56+
services: HandleTokenServices,
57+
params: GetTokenRequest
58+
) {
59+
const clientId = params.get("client_id");
60+
const clientSecret = params.get("client_secret");
61+
const userPoolClient = await services.cognito.getAppClient(ctx, clientId);
62+
if (!userPoolClient) {
63+
throw new NotAuthorizedError();
64+
}
65+
if (
66+
userPoolClient.ClientSecret &&
67+
userPoolClient.ClientSecret != clientSecret
68+
) {
69+
throw new NotAuthorizedError();
70+
}
71+
72+
const tokens = await services.tokenGenerator.generateWithClientCreds(
73+
ctx,
74+
userPoolClient
75+
);
76+
console.log("Tokens:", tokens);
77+
if (!tokens) {
78+
throw new NotAuthorizedError();
79+
}
80+
81+
return {
82+
access_token: tokens.AccessToken,
83+
};
84+
}
85+
5486
export const GetToken =
5587
(services: HandleTokenServices): GetTokenTarget =>
5688
async (ctx, req) => {
@@ -60,12 +92,13 @@ export const GetToken =
6092
throw new NotImplementedError();
6193
}
6294
case "client_credentials": {
63-
throw new NotImplementedError();
95+
return getWithClientCredentials(ctx, services, params);
6496
}
6597
case "refresh_token": {
66-
return getRefreshToken(ctx, services, params);
98+
return getWithRefreshToken(ctx, services, params);
6799
}
68100
default: {
101+
console.log("Invalid grant type passed:", params.get("grant_type"));
69102
throw new InvalidParameterError();
70103
}
71104
}

0 commit comments

Comments
 (0)