Skip to content

Commit 495056a

Browse files
authoredDec 6, 2023
fix: handle clock skew (#87)
GitHub's macOS runners for the past while have had some bad clock drift which sometimes prevents this action from working with the error: ```console 'Issued at' claim ('iat') must be an Integer representing the time that the assertion was issued ``` `@octokit/auth-app` already has logic to handle this so we can defer to that code.
1 parent 8746053 commit 495056a

File tree

5 files changed

+107
-29
lines changed

5 files changed

+107
-29
lines changed
 

‎dist/main.cjs

+10-13
Original file line numberDiff line numberDiff line change
@@ -10390,12 +10390,9 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp
1039010390
privateKey: privateKey2,
1039110391
request: request2
1039210392
});
10393-
const appAuthentication = await auth({
10394-
type: "app"
10395-
});
1039610393
let authentication;
1039710394
if (parsedRepositoryNames) {
10398-
authentication = await pRetry(() => getTokenFromRepository(request2, auth, parsedOwner, appAuthentication, parsedRepositoryNames), {
10395+
authentication = await pRetry(() => getTokenFromRepository(request2, auth, parsedOwner, parsedRepositoryNames), {
1039910396
onFailedAttempt: (error) => {
1040010397
core2.info(
1040110398
`Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}`
@@ -10404,7 +10401,7 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp
1040410401
retries: 3
1040510402
});
1040610403
} else {
10407-
authentication = await pRetry(() => getTokenFromOwner(request2, auth, appAuthentication, parsedOwner), {
10404+
authentication = await pRetry(() => getTokenFromOwner(request2, auth, parsedOwner), {
1040810405
onFailedAttempt: (error) => {
1040910406
core2.info(
1041010407
`Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}`
@@ -10419,19 +10416,19 @@ async function main(appId2, privateKey2, owner2, repositories2, core2, createApp
1041910416
core2.saveState("token", authentication.token);
1042010417
}
1042110418
}
10422-
async function getTokenFromOwner(request2, auth, appAuthentication, parsedOwner) {
10419+
async function getTokenFromOwner(request2, auth, parsedOwner) {
1042310420
const response = await request2("GET /orgs/{org}/installation", {
1042410421
org: parsedOwner,
10425-
headers: {
10426-
authorization: `bearer ${appAuthentication.token}`
10422+
request: {
10423+
hook: auth.hook
1042710424
}
1042810425
}).catch((error) => {
1042910426
if (error.status !== 404)
1043010427
throw error;
1043110428
return request2("GET /users/{username}/installation", {
1043210429
username: parsedOwner,
10433-
headers: {
10434-
authorization: `bearer ${appAuthentication.token}`
10430+
request: {
10431+
hook: auth.hook
1043510432
}
1043610433
});
1043710434
});
@@ -10441,12 +10438,12 @@ async function getTokenFromOwner(request2, auth, appAuthentication, parsedOwner)
1044110438
});
1044210439
return authentication;
1044310440
}
10444-
async function getTokenFromRepository(request2, auth, parsedOwner, appAuthentication, parsedRepositoryNames) {
10441+
async function getTokenFromRepository(request2, auth, parsedOwner, parsedRepositoryNames) {
1044510442
const response = await request2("GET /repos/{owner}/{repo}/installation", {
1044610443
owner: parsedOwner,
1044710444
repo: parsedRepositoryNames.split(",")[0],
10448-
headers: {
10449-
authorization: `bearer ${appAuthentication.token}`
10445+
request: {
10446+
hook: auth.hook
1045010447
}
1045110448
});
1045210449
const authentication = await auth({

‎lib/main.js

+10-14
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,11 @@ export async function main(
7070
request,
7171
});
7272

73-
const appAuthentication = await auth({
74-
type: "app",
75-
});
76-
7773
let authentication;
7874
// If at least one repository is set, get installation ID from that repository
7975

8076
if (parsedRepositoryNames) {
81-
authentication = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner,appAuthentication, parsedRepositoryNames), {
77+
authentication = await pRetry(() => getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames), {
8278
onFailedAttempt: (error) => {
8379
core.info(
8480
`Failed to create token for "${parsedRepositoryNames}" (attempt ${error.attemptNumber}): ${error.message}`
@@ -89,7 +85,7 @@ export async function main(
8985

9086
} else {
9187
// Otherwise get the installation for the owner, which can either be an organization or a user account
92-
authentication = await pRetry(() => getTokenFromOwner(request, auth, appAuthentication, parsedOwner), {
88+
authentication = await pRetry(() => getTokenFromOwner(request, auth, parsedOwner), {
9389
onFailedAttempt: (error) => {
9490
core.info(
9591
`Failed to create token for "${parsedOwner}" (attempt ${error.attemptNumber}): ${error.message}`
@@ -110,12 +106,12 @@ export async function main(
110106
}
111107
}
112108

113-
async function getTokenFromOwner(request, auth, appAuthentication, parsedOwner) {
109+
async function getTokenFromOwner(request, auth, parsedOwner) {
114110
// https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-organization-installation-for-the-authenticated-app
115111
const response = await request("GET /orgs/{org}/installation", {
116112
org: parsedOwner,
117-
headers: {
118-
authorization: `bearer ${appAuthentication.token}`,
113+
request: {
114+
hook: auth.hook,
119115
},
120116
}).catch((error) => {
121117
/* c8 ignore next */
@@ -124,8 +120,8 @@ async function getTokenFromOwner(request, auth, appAuthentication, parsedOwner)
124120
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-user-installation-for-the-authenticated-app
125121
return request("GET /users/{username}/installation", {
126122
username: parsedOwner,
127-
headers: {
128-
authorization: `bearer ${appAuthentication.token}`,
123+
request: {
124+
hook: auth.hook,
129125
},
130126
});
131127
});
@@ -138,13 +134,13 @@ async function getTokenFromOwner(request, auth, appAuthentication, parsedOwner)
138134
return authentication;
139135
}
140136

141-
async function getTokenFromRepository(request, auth, parsedOwner,appAuthentication, parsedRepositoryNames) {
137+
async function getTokenFromRepository(request, auth, parsedOwner, parsedRepositoryNames) {
142138
// https://docs.github.com/rest/apps/apps?apiVersion=2022-11-28#get-a-repository-installation-for-the-authenticated-app
143139
const response = await request("GET /repos/{owner}/{repo}/installation", {
144140
owner: parsedOwner,
145141
repo: parsedRepositoryNames.split(",")[0],
146-
headers: {
147-
authorization: `bearer ${appAuthentication.token}`,
142+
request: {
143+
hook: auth.hook,
148144
},
149145
});
150146

‎package-lock.json

+30-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"p-retry": "^6.1.0"
1919
},
2020
"devDependencies": {
21+
"@sinonjs/fake-timers": "^11.2.2",
2122
"ava": "^5.3.1",
2223
"c8": "^8.0.1",
2324
"dotenv": "^16.3.1",

‎tests/main-repo-skew.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { test } from "./main.js";
2+
3+
import { install } from "@sinonjs/fake-timers";
4+
5+
// Verify `main` retry when the clock has drifted.
6+
await test((mockPool) => {
7+
process.env.INPUT_OWNER = 'actions'
8+
process.env.INPUT_REPOSITORIES = 'failed-repo';
9+
const owner = process.env.INPUT_OWNER
10+
const repo = process.env.INPUT_REPOSITORIES
11+
const mockInstallationId = "123456";
12+
13+
install({ now: 0, toFake: ["Date"] });
14+
15+
mockPool
16+
.intercept({
17+
path: `/repos/${owner}/${repo}/installation`,
18+
method: "GET",
19+
headers: {
20+
accept: "application/vnd.github.v3+json",
21+
"user-agent": "actions/create-github-app-token",
22+
// Intentionally omitting the `authorization` header, since JWT creation is not idempotent.
23+
},
24+
})
25+
.reply(({ headers }) => {
26+
const [_, jwt] = (headers.authorization || "").split(" ");
27+
const payload = JSON.parse(Buffer.from(jwt.split(".")[1], "base64").toString());
28+
29+
if (payload.iat < 0) {
30+
return {
31+
statusCode: 401,
32+
data: {
33+
message: "'Issued at' claim ('iat') must be an Integer representing the time that the assertion was issued."
34+
},
35+
responseOptions: {
36+
headers: {
37+
"content-type": "application/json",
38+
"date": new Date(Date.now() + 30000).toUTCString()
39+
}
40+
}
41+
};
42+
}
43+
44+
return {
45+
statusCode: 200,
46+
data: {
47+
id: mockInstallationId
48+
},
49+
responseOptions: {
50+
headers: {
51+
"content-type": "application/json"
52+
}
53+
}
54+
};
55+
}).times(2);
56+
});

0 commit comments

Comments
 (0)
Please sign in to comment.