Skip to content

Commit 054749d

Browse files
build: add jwsSign function in auth (#808)
1 parent 4b4ec05 commit 054749d

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {TokenOptions} from './tokenOptions';
16+
import {sign, SignOptions} from 'jws';
17+
18+
/** The default algorithm for signing JWTs. */
19+
const ALG_RS256 = 'RS256';
20+
/** The URL for Google's OAuth 2.0 token endpoint. */
21+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
22+
23+
/**
24+
* Interface for the JWT payload required for signing.
25+
*/
26+
interface JwsSignPayload {
27+
/** The issuer claim for the JWT. */
28+
iss?: string;
29+
/** The space-delimited list of scopes for the requested token. */
30+
scope?: string | string[];
31+
/** The audience for the token. */
32+
aud: string;
33+
/** The expiration time of the token, in seconds since the epoch. */
34+
exp: number;
35+
/** The time the token was issued, in seconds since the epoch. */
36+
iat: number;
37+
/** The subject claim for the JWT, used for impersonation. */
38+
sub?: string;
39+
/** Additional claims to include in the JWT payload. */
40+
[key: string]: any;
41+
}
42+
43+
/**
44+
* Builds the JWT payload for signing.
45+
* @param tokenOptions The options for the token.
46+
* @returns The JWT payload.
47+
*/
48+
function buildPayloadForJwsSign(tokenOptions: TokenOptions): JwsSignPayload {
49+
const iat = Math.floor(new Date().getTime() / 1000);
50+
const payload: JwsSignPayload = {
51+
iss: tokenOptions.iss,
52+
scope: tokenOptions.scope,
53+
aud: GOOGLE_TOKEN_URL,
54+
exp: iat + 3600,
55+
iat,
56+
sub: tokenOptions.sub,
57+
...tokenOptions.additionalClaims,
58+
};
59+
return payload;
60+
}
61+
62+
/**
63+
* Creates a signed JWS (JSON Web Signature).
64+
* @param tokenOptions The options for the token.
65+
* @returns The signed JWS.
66+
*/
67+
function getJwsSign(tokenOptions: TokenOptions): string {
68+
const payload: JwsSignPayload = buildPayloadForJwsSign(tokenOptions);
69+
return sign({
70+
header: {alg: ALG_RS256},
71+
payload,
72+
secret: tokenOptions.key,
73+
} as SignOptions);
74+
}
75+
76+
export {buildPayloadForJwsSign, getJwsSign};
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import * as fs from 'fs';
16+
import * as assert from 'assert';
17+
import {describe, it, afterEach} from 'mocha';
18+
import * as sinon from 'sinon';
19+
import * as jws from 'jws';
20+
import {buildPayloadForJwsSign, getJwsSign} from '../../src/gtoken/jwsSign';
21+
import {TokenOptions} from '../../src/gtoken/tokenOptions';
22+
23+
describe('jwsSign', () => {
24+
const sandbox = sinon.createSandbox();
25+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
26+
const privateKey = fs.readFileSync('./test/fixtures/private.pem', 'utf-8');
27+
const FAKE_KEY =
28+
'-----BEGIN PRIVATE KEY-----\nFAKEKEY\n-----END PRIVATE KEY-----\n';
29+
30+
afterEach(() => {
31+
sandbox.restore();
32+
});
33+
34+
describe('buildPayloadForJwsSign', () => {
35+
it('should build a minimal payload', () => {
36+
const clock = sandbox.useFakeTimers(new Date().getTime());
37+
const iat = Math.floor(clock.now / 1000);
38+
const tokenOptions: TokenOptions = {};
39+
const payload = buildPayloadForJwsSign(tokenOptions);
40+
assert.deepStrictEqual(payload, {
41+
iss: undefined,
42+
scope: undefined,
43+
aud: GOOGLE_TOKEN_URL,
44+
exp: iat + 3600,
45+
iat,
46+
sub: undefined,
47+
});
48+
});
49+
50+
it('should include iss, scope, and sub from tokenOptions', () => {
51+
const clock = sandbox.useFakeTimers(new Date().getTime());
52+
const iat = Math.floor(clock.now / 1000);
53+
const tokenOptions: TokenOptions = {
54+
iss: 'test-issuer',
55+
scope: 'test-scope',
56+
sub: 'test-subject',
57+
};
58+
const payload = buildPayloadForJwsSign(tokenOptions);
59+
assert.deepStrictEqual(payload, {
60+
iss: 'test-issuer',
61+
scope: 'test-scope',
62+
aud: GOOGLE_TOKEN_URL,
63+
exp: iat + 3600,
64+
iat,
65+
sub: 'test-subject',
66+
});
67+
});
68+
69+
it('should include additional claims from tokenOptions', () => {
70+
const clock = sandbox.useFakeTimers(new Date().getTime());
71+
const iat = Math.floor(clock.now / 1000);
72+
const tokenOptions: TokenOptions = {
73+
additionalClaims: {
74+
claim1: 'value1',
75+
claim2: 123,
76+
},
77+
};
78+
const payload = buildPayloadForJwsSign(tokenOptions);
79+
assert.deepStrictEqual(payload, {
80+
iss: undefined,
81+
scope: undefined,
82+
aud: GOOGLE_TOKEN_URL,
83+
exp: iat + 3600,
84+
iat,
85+
sub: undefined,
86+
claim1: 'value1',
87+
claim2: 123,
88+
});
89+
});
90+
});
91+
92+
describe('getJwsSign', () => {
93+
it('should return a signed JWS', () => {
94+
const tokenOptions: TokenOptions = {
95+
iss: 'test-issuer',
96+
scope: 'test-scope',
97+
key: privateKey,
98+
};
99+
const signedJws = getJwsSign(tokenOptions);
100+
const decoded = jws.decode(signedJws);
101+
assert(decoded);
102+
assert.strictEqual(typeof decoded.payload, 'string');
103+
const payload = JSON.parse(decoded.payload as string);
104+
assert.deepStrictEqual(decoded.header, {alg: 'RS256'});
105+
assert.strictEqual(payload.iss, 'test-issuer');
106+
assert.strictEqual(payload.scope, 'test-scope');
107+
assert.strictEqual(payload.aud, GOOGLE_TOKEN_URL);
108+
});
109+
});
110+
});

0 commit comments

Comments
 (0)