forked from docker/login-action
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdockerhub.ts
More file actions
137 lines (121 loc) · 5.21 KB
/
Copy pathdockerhub.ts
File metadata and controls
137 lines (121 loc) · 5.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import * as core from '@actions/core';
import * as httpm from '@actions/http-client';
import {HttpCodes} from '@actions/http-client';
import {validate as uuidValidate} from 'uuid';
export interface LoginCredentials {
username: string;
token: string;
}
interface OIDCTokenResponse {
access_token: string;
}
const registries = new Set(['', 'docker.io', 'registry-1.docker.io', 'registry-1-stage.docker.io', 'dhi.io']);
const defaultExpiresIn = 300;
const minExpiresIn = 300;
const maxExpiresIn = 21600;
const maxRetries = 5;
export const isDockerHubOIDC = (registry: string, password: string): boolean => {
return process.env.DOCKERHUB_OIDC_CONNECTIONID !== undefined && !password && registries.has(registry);
};
export const getOIDCToken = async (registry: string, username: string): Promise<LoginCredentials> => {
const connectionID = process.env.DOCKERHUB_OIDC_CONNECTIONID?.trim();
if (!connectionID) {
throw new Error('DOCKERHUB_OIDC_CONNECTIONID is required for Docker Hub OIDC login');
}
if (!uuidValidate(connectionID)) {
throw new Error('Invalid DOCKERHUB_OIDC_CONNECTIONID. Must be a valid UUID.');
}
const expiresIn = getExpiresIn();
const identityHost = registry === 'registry-1-stage.docker.io' ? 'identity-stage.docker.com' : 'identity.docker.com';
const audience = `https://${identityHost}`;
core.info(`Docker Hub OIDC detected for ${registry || 'docker.io'}`);
core.debug(`Docker Hub OIDC token audience: ${audience}`);
core.debug(`Docker Hub OIDC token expiration: ${expiresIn}s`);
core.info(`Retrieving GitHub OIDC token for Docker Hub`);
const idToken = await core.getIDToken(audience);
const http: httpm.HttpClient = new httpm.HttpClient('github.com/docker/login-action', [], {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
const data = new URLSearchParams();
data.set('grant_type', 'urn:ietf:params:oauth:grant-type:token-exchange');
data.set('subject_token_type', 'urn:ietf:params:oauth:token-type:id_token');
data.set('subject_token', idToken);
data.set('connection_id', connectionID);
data.set('expires_in', expiresIn.toString());
core.info(`Exchanging GitHub OIDC token for Docker Hub token`);
const resp = await postWithRetry(http, `https://${identityHost}/oauth/token`, data.toString());
const tokenResp = <OIDCTokenResponse>JSON.parse(await handleResponse(resp));
core.setSecret(tokenResp.access_token);
core.info(`Docker Hub OIDC token exchange succeeded`);
return {
username,
token: tokenResp.access_token
};
};
const getExpiresIn = (): number => {
const expiresInInput = process.env.DOCKERHUB_OIDC_EXPIREIN?.trim() || defaultExpiresIn.toString();
const expiresIn = Number(expiresInInput);
if (isNaN(expiresIn) || expiresIn < minExpiresIn || expiresIn > maxExpiresIn) {
throw new Error(`Invalid DOCKERHUB_OIDC_EXPIREIN: ${expiresInInput}. Must be between ${minExpiresIn} and ${maxExpiresIn}`);
}
return expiresIn;
};
const postWithRetry = async (http: httpm.HttpClient, url: string, data: string): Promise<httpm.HttpClientResponse> => {
core.debug(`Sending Docker Hub OIDC token request to ${url}`);
let resp = await http.post(url, data);
core.debug(`Docker Hub OIDC token request returned status code ${resp.message.statusCode || HttpCodes.InternalServerError}`);
for (let attempt = 0; (resp.message.statusCode || HttpCodes.InternalServerError) === HttpCodes.TooManyRequests && attempt < maxRetries; attempt++) {
const delay = parseRetryAfter(resp.message.headers['retry-after']);
if (delay === null) {
core.debug(`Docker Hub OIDC token request rate limited without retry-after header`);
break;
}
await resp.readBody();
core.info(`Docker Hub OIDC token request rate limited, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
resp = await http.post(url, data);
core.debug(`Docker Hub OIDC token request returned status code ${resp.message.statusCode || HttpCodes.InternalServerError}`);
}
return resp;
};
const parseRetryAfter = (value: string | string[] | undefined): number | null => {
if (value === undefined) {
return null;
}
if (Array.isArray(value)) {
value = value[0];
}
const seconds = Number(value);
if (isNaN(seconds)) {
return null;
}
return Math.max(0, seconds * 1000);
};
const handleResponse = async (resp: httpm.HttpClientResponse): Promise<string> => {
const body = await resp.readBody();
const statusCode = resp.message.statusCode || HttpCodes.InternalServerError;
core.debug(`Docker Hub OIDC token response status code: ${statusCode}`);
if (statusCode < HttpCodes.OK || statusCode >= HttpCodes.MultipleChoices) {
throw parseError(statusCode, body);
}
return body;
};
const parseError = (statusCode: number, body: string): Error => {
if (body) {
let errResp: unknown;
try {
errResp = JSON.parse(body);
} catch {
errResp = undefined;
}
if (errResp !== undefined) {
throw new Error(`Docker Hub API: bad status code ${statusCode}: ${JSON.stringify(errResp)}`);
}
}
if (statusCode === 401) {
throw new Error(`Docker Hub API: operation not permitted`);
}
throw new Error(`Docker Hub API: bad status code ${statusCode}`);
};