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 defaultExpiresIn = 300; const minExpiresIn = 300; const maxExpiresIn = 3600; const maxRetries = 5; export const isDockerHubOIDC = (registry: string, password: string): boolean => { return process.env.DOCKERHUB_OIDC_CONNECTIONID !== undefined && !password && isDockerHubRegistry(registry); }; const isDockerHubRegistry = (registry: string): boolean => { return registry === '' || registry === 'docker.io' || registry === 'registry-1.docker.io' || registry === 'registry-1-stage.docker.io'; }; export const getOIDCToken = async (registry: string, username: string): Promise => { 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}`; 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()); const resp = await postWithRetry(http, `https://${identityHost}/oauth/token`, data.toString()); const tokenResp = JSON.parse(await handleResponse(resp)); core.setSecret(tokenResp.access_token); 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 => { let resp = await http.post(url, data); 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) { 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); } 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 => { const body = await resp.readBody(); const statusCode = resp.message.statusCode || HttpCodes.InternalServerError; if (statusCode < HttpCodes.OK || statusCode >= HttpCodes.MultipleChoices) { throw parseError(statusCode, body); } return body; }; const parseError = (statusCode: number, body: string): Error => { if (statusCode === 401) { throw new Error(`Docker Hub API: operation not permitted`); } if (body) { const errResp = parseErrorBody(body); for (const k of ['description', 'message', 'detail', 'error']) { if (errResp[k]) { throw new Error(`Docker Hub API: bad status code ${statusCode}: ${errResp[k]}`); } } } throw new Error(`Docker Hub API: bad status code ${statusCode}`); }; const parseErrorBody = (body: string): Record => { try { return >JSON.parse(body); } catch { return {}; } };