diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ed99a0dc..36a3f4dd 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,6 +7,10 @@ jobs: provider: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + id-token: write + contents: read + environment: developement steps: - name: Checkout uses: actions/checkout@v2 diff --git a/action.yml b/action.yml index 6d471710..dc4024df 100644 --- a/action.yml +++ b/action.yml @@ -9,6 +9,9 @@ inputs: provider-endpoint: description: "URL for credential provider" required: false + audience: + description: "the audience of id token" + required: false outputs: token: description: An installation token for the GitHub App on the requested repository. diff --git a/action/__test__/index.test.ts b/action/__test__/index.test.ts index b8481876..41909b07 100644 --- a/action/__test__/index.test.ts +++ b/action/__test__/index.test.ts @@ -65,6 +65,7 @@ describe("tests", () => { await index.assumeRole({ githubToken: "ghs_dummyGitHubToken", providerEndpoint: "http://localhost:8080", + audience: "", }); expect(core.setSecret).toHaveBeenCalledWith("ghs_dummyGitHubToken"); }); diff --git a/action/package-lock.json b/action/package-lock.json index afa09190..9ab8e3f9 100644 --- a/action/package-lock.json +++ b/action/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@actions/core": "^1.5.0", + "@actions/core": "^1.6.0-beta.0", "@actions/http-client": "^1.0.11" }, "devDependencies": { @@ -24,9 +24,12 @@ } }, "node_modules/@actions/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.5.0.tgz", - "integrity": "sha512-eDOLH1Nq9zh+PJlYLqEMkS/jLQxhksPNmUGNBHfa4G+tQmnIhzpctxmchETtVGyBOvXgOVVpYuE40+eS4cUnwQ==" + "version": "1.6.0-beta.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0-beta.0.tgz", + "integrity": "sha512-NaJq0c65C8POKyHIDCa+/2RuYDSsTW11bfB2/UDPba529qJJGxOFBxjqIUY2YVqlI9umRta9ufgEIJndyQ4ZAA==", + "dependencies": { + "@actions/http-client": "^1.0.11" + } }, "node_modules/@actions/exec": { "version": "1.1.0", @@ -4037,9 +4040,12 @@ }, "dependencies": { "@actions/core": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.5.0.tgz", - "integrity": "sha512-eDOLH1Nq9zh+PJlYLqEMkS/jLQxhksPNmUGNBHfa4G+tQmnIhzpctxmchETtVGyBOvXgOVVpYuE40+eS4cUnwQ==" + "version": "1.6.0-beta.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0-beta.0.tgz", + "integrity": "sha512-NaJq0c65C8POKyHIDCa+/2RuYDSsTW11bfB2/UDPba529qJJGxOFBxjqIUY2YVqlI9umRta9ufgEIJndyQ4ZAA==", + "requires": { + "@actions/http-client": "^1.0.11" + } }, "@actions/exec": { "version": "1.1.0", diff --git a/action/package.json b/action/package.json index 40965058..7af2dfcb 100644 --- a/action/package.json +++ b/action/package.json @@ -11,7 +11,7 @@ "author": "Shogo Ichinose", "license": "MIT", "dependencies": { - "@actions/core": "^1.5.0", + "@actions/core": "^1.6.0-beta.0", "@actions/http-client": "^1.0.11" }, "devDependencies": { diff --git a/action/src/index.ts b/action/src/index.ts index 865198ce..36b8a5f6 100644 --- a/action/src/index.ts +++ b/action/src/index.ts @@ -4,10 +4,10 @@ import * as http from "@actions/http-client"; interface GetTokenParams { githubToken: string; providerEndpoint: string; + audience: string; } interface GetTokenPayload { - github_token: string; api_url: string; repository: string; sha: string; @@ -71,20 +71,29 @@ export async function assumeRole(params: GetTokenParams) { const { GITHUB_REPOSITORY, GITHUB_SHA } = process.env; assertIsDefined(GITHUB_REPOSITORY); assertIsDefined(GITHUB_SHA); - validateGitHubToken(params.githubToken); const GITHUB_API_URL = process.env["GITHUB_API_URL"] || "https://api.github.com"; const payload: GetTokenPayload = { - github_token: params.githubToken, api_url: GITHUB_API_URL, repository: GITHUB_REPOSITORY, sha: GITHUB_SHA, }; + const headers: { [name: string]: string } = {}; + + let token: string; + if (isIdTokenAvailable()) { + token = await core.getIDToken(params.audience); + } else { + validateGitHubToken(params.githubToken); + token = params.githubToken; + } + headers["Authorization"] = `Bearer ${token}`; + const client = new http.HttpClient("actions-github-app-token"); - const result = await client.postJson(params.providerEndpoint, payload); - if (result.statusCode !== 200) { + const result = await client.postJson(params.providerEndpoint, payload, headers); + if (result.statusCode !== http.HttpCodes.OK) { const resp = result.result as GetTokenError; - core.setFailed(resp.message); + core.setFailed(resp?.message || "unknown error"); return; } const resp = result.result as GetTokenResult; @@ -102,6 +111,12 @@ export async function assumeRole(params: GetTokenParams) { core.saveState("token", resp.github_token); } +function isIdTokenAvailable(): boolean { + const token = process.env["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]; + const url = process.env["ACTIONS_ID_TOKEN_REQUEST_URL"]; + return token && url ? true : false; +} + async function run() { try { const required = { @@ -110,9 +125,12 @@ async function run() { const githubToken = core.getInput("github-token", required); const providerEndpoint = core.getInput("provider-endpoint") || "https://aznfkxv2k8.execute-api.us-east-1.amazonaws.com/"; + const audience = core.getInput("audience", { required: false }); + await assumeRole({ githubToken, providerEndpoint, + audience, }); } catch (error) { if (error instanceof Error) { diff --git a/provider/github-app-token/dummy.go b/provider/github-app-token/dummy.go index 3c7234e2..e5de3192 100644 --- a/provider/github-app-token/dummy.go +++ b/provider/github-app-token/dummy.go @@ -2,6 +2,7 @@ package githubapptoken import ( "context" + "errors" "net/http" "github.com/shogo82148/actions-github-app-token/provider/github-app-token/github" @@ -44,6 +45,10 @@ func (c *githubClientDummy) ValidateAPIURL(url string) error { return nil } +func (c *githubClientDummy) ParseIDToken(ctx context.Context, idToken string) (*github.ActionsIDToken, error) { + return nil, errors.New("invalid jwt") +} + func NewDummyHandler() *Handler { return &Handler{ github: &githubClientDummy{}, diff --git a/provider/github-app-token/github-app-token.go b/provider/github-app-token/github-app-token.go index 1dd754ca..c567d091 100644 --- a/provider/github-app-token/github-app-token.go +++ b/provider/github-app-token/github-app-token.go @@ -24,6 +24,7 @@ type githubClient interface { GetReposInstallation(ctx context.Context, owner, repo string) (*github.GetReposInstallationResponse, error) CreateAppAccessToken(ctx context.Context, installationID uint64, permissions *github.CreateAppAccessTokenRequest) (*github.CreateAppAccessTokenResponse, error) ValidateAPIURL(url string) error + ParseIDToken(ctx context.Context, idToken string) (*github.ActionsIDToken, error) } const ( @@ -85,10 +86,9 @@ func NewHandler() (*Handler, error) { } type requestBody struct { - GitHubToken string `json:"github_token"` - Repository string `json:"repository"` - SHA string `json:"sha"` - APIURL string `json:"api_url"` + Repository string `json:"repository"` + SHA string `json:"sha"` + APIURL string `json:"api_url"` } type responseBody struct { @@ -129,8 +129,13 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { }) return } + token, err := h.getAuthToken(r.Header) + if err != nil { + h.handleError(w, r, err) + return + } - resp, err := h.handle(ctx, payload) + resp, err := h.handle(ctx, token, payload) if err != nil { h.handleError(w, r, err) return @@ -142,20 +147,32 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -func (h *Handler) handle(ctx context.Context, req *requestBody) (*responseBody, error) { +func (h *Handler) handle(ctx context.Context, token string, req *requestBody) (*responseBody, error) { if err := h.github.ValidateAPIURL(req.APIURL); err != nil { return nil, &validationError{ message: err.Error(), } } - if err := h.validateGitHubToken(ctx, req); err != nil { - return nil, err - } - owner, repo, err := splitOwnerRepo(req.Repository) - if err != nil { - return nil, err + // authorize the request + var owner, repo string + if id, err := h.github.ParseIDToken(ctx, token); err == nil { + owner, repo, err = splitOwnerRepo(id.Repository) + if err != nil { + return nil, err + } + } else { + err := h.validateGitHubToken(ctx, token, req) + if err != nil { + return nil, err + } + owner, repo, err = splitOwnerRepo(req.Repository) + if err != nil { + return nil, err + } } + + // issue a new access token inst, err := h.github.GetReposInstallation(ctx, owner, repo) if err != nil { var ghErr *github.ErrUnexpectedStatusCode @@ -173,7 +190,7 @@ func (h *Handler) handle(ctx context.Context, req *requestBody) (*responseBody, } return nil, fmt.Errorf("failed to get resp's installation: %w", err) } - token, err := h.github.CreateAppAccessToken(ctx, inst.ID, &github.CreateAppAccessTokenRequest{ + resp, err := h.github.CreateAppAccessToken(ctx, inst.ID, &github.CreateAppAccessTokenRequest{ Repositories: []string{repo}, }) if err != nil { @@ -181,7 +198,7 @@ func (h *Handler) handle(ctx context.Context, req *requestBody) (*responseBody, } return &responseBody{ - GitHubToken: token.Token, + GitHubToken: resp.Token, }, nil } @@ -227,15 +244,31 @@ func (h *Handler) handleMethodNotAllowed(w http.ResponseWriter) { w.Write(data) } -func (h *Handler) validateGitHubToken(ctx context.Context, req *requestBody) error { +func (h *Handler) getAuthToken(header http.Header) (string, error) { + const prefix = "Bearer " + v := header.Get("Authorization") + if len(v) < len(prefix) { + return "", &validationError{ + message: "invalid Authorization header", + } + } + if !strings.EqualFold(v[:len(prefix)], prefix) { + return "", &validationError{ + message: "invalid Authorization header", + } + } + return v[len(prefix):], nil +} + +func (h *Handler) validateGitHubToken(ctx context.Context, token string, req *requestBody) error { // early check of the token prefix // ref. https://github.blog/changelog/2021-03-31-authentication-token-format-updates-are-generally-available/ - if len(req.GitHubToken) < 4 { + if len(token) < 4 { return &validationError{ message: "GITHUB_TOKEN has invalid format", } } - switch req.GitHubToken[:4] { + switch token[:4] { case "ghp_": // Personal Access Tokens return &validationError{ @@ -265,9 +298,9 @@ func (h *Handler) validateGitHubToken(ctx context.Context, req *requestBody) err message: "GITHUB_TOKEN looks like Personal Access Token. `github-token` must be `${{ github.token }}` or `${{ secrets.GITHUB_TOKEN }}`.", } } - resp, err := h.updateCommitStatus(ctx, req, &github.CreateStatusRequest{ + resp, err := h.updateCommitStatus(ctx, token, req, &github.CreateStatusRequest{ State: github.CommitStateSuccess, - Description: "valid github token", + Description: "valid GitHub token", Context: commitStatusContext, }) if err != nil { @@ -302,10 +335,10 @@ func splitOwnerRepo(fullname string) (owner, repo string, err error) { return } -func (h *Handler) updateCommitStatus(ctx context.Context, req *requestBody, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error) { +func (h *Handler) updateCommitStatus(ctx context.Context, token string, req *requestBody, status *github.CreateStatusRequest) (*github.CreateStatusResponse, error) { owner, repo, err := splitOwnerRepo(req.Repository) if err != nil { return nil, err } - return h.github.CreateStatus(ctx, req.GitHubToken, owner, repo, req.SHA, status) + return h.github.CreateStatus(ctx, token, owner, repo, req.SHA, status) } diff --git a/provider/github-app-token/github-app-token_test.go b/provider/github-app-token/github-app-token_test.go index 9b8681b2..140aa9f4 100644 --- a/provider/github-app-token/github-app-token_test.go +++ b/provider/github-app-token/github-app-token_test.go @@ -15,6 +15,7 @@ type githubClientMock struct { GetReposInstallationFunc func(ctx context.Context, owner, repo string) (*github.GetReposInstallationResponse, error) CreateAppAccessTokenFunc func(ctx context.Context, installationID uint64, permissions *github.CreateAppAccessTokenRequest) (*github.CreateAppAccessTokenResponse, error) ValidateAPIURLFunc func(url string) error + ParseIDTokenFunc func(ctx context.Context, idToken string) (*github.ActionsIDToken, error) } func (c *githubClientMock) GetApp(ctx context.Context) (*github.GetAppResponse, error) { @@ -37,6 +38,10 @@ func (c *githubClientMock) ValidateAPIURL(url string) error { return c.ValidateAPIURLFunc(url) } +func (c *githubClientMock) ParseIDToken(ctx context.Context, idToken string) (*github.ActionsIDToken, error) { + return c.ParseIDTokenFunc(ctx, idToken) +} + func TestValidateGitHubToken(t *testing.T) { h := &Handler{ github: &githubClientMock{ @@ -72,10 +77,9 @@ func TestValidateGitHubToken(t *testing.T) { }, }, } - err := h.validateGitHubToken(context.Background(), &requestBody{ - GitHubToken: "ghs_dummyGitHubToken", - Repository: "fuller-inc/actions-aws-assume-role", - SHA: "e3a45c6c16c1464826b36a598ff39e6cc98c4da4", + err := h.validateGitHubToken(context.Background(), "ghs_dummyGitHubToken", &requestBody{ + Repository: "fuller-inc/actions-aws-assume-role", + SHA: "e3a45c6c16c1464826b36a598ff39e6cc98c4da4", }) if err != nil { t.Error(err) @@ -92,10 +96,9 @@ func TestValidateGitHubToken_PermissionError(t *testing.T) { }, }, } - err := h.validateGitHubToken(context.Background(), &requestBody{ - GitHubToken: "ghs_dummyGitHubToken", - Repository: "fuller-inc/actions-aws-assume-role", - SHA: "e3a45c6c16c1464826b36a598ff39e6cc98c4da4", + err := h.validateGitHubToken(context.Background(), "ghs_dummyGitHubToken", &requestBody{ + Repository: "fuller-inc/actions-aws-assume-role", + SHA: "e3a45c6c16c1464826b36a598ff39e6cc98c4da4", }) if err == nil { t.Error("want error, but not") @@ -124,10 +127,9 @@ func TestValidateGitHubToken_InvalidCreator(t *testing.T) { }, }, } - err := h.validateGitHubToken(context.Background(), &requestBody{ - GitHubToken: "ghs_dummyGitHubToken", - Repository: "fuller-inc/actions-aws-assume-role", - SHA: "e3a45c6c16c1464826b36a598ff39e6cc98c4da4", + err := h.validateGitHubToken(context.Background(), "ghs_dummyGitHubToken", &requestBody{ + Repository: "fuller-inc/actions-aws-assume-role", + SHA: "e3a45c6c16c1464826b36a598ff39e6cc98c4da4", }) if err == nil { t.Error("want error, but not") diff --git a/provider/github-app-token/github/github.go b/provider/github-app-token/github/github.go index bc25b43c..39731ea7 100644 --- a/provider/github-app-token/github/github.go +++ b/provider/github-app-token/github/github.go @@ -19,10 +19,17 @@ import ( ) const ( - githubUserAgent = "actions-github-token/1.0" + // The value of User-Agent header + githubUserAgent = "actions-github-token/1.0" + + // The default url of Github API defaultAPIBaseURL = "https://api.github.com" + + oidcIssuer = "https://vstoken.actions.githubusercontent.com" ) +var oidcThumbprints = []string{"a031c46782e6e6c662c2c87c76da9aa62ccabd8e"} + var apiBaseURL string func init() { @@ -40,10 +47,16 @@ func init() { // Client is a very light weight GitHub API Client. type Client struct { - baseURL string - httpClient *http.Client + baseURL string + httpClient *http.Client + + // configure for GitHub App appID uint64 rsaPrivateKey *rsa.PrivateKey + + // configure for OpenID Connect + issuer string + thumbprints []string } func NewClient(httpClient *http.Client, appID uint64, privateKey []byte) (*Client, error) { @@ -51,9 +64,11 @@ func NewClient(httpClient *http.Client, appID uint64, privateKey []byte) (*Clien httpClient = http.DefaultClient } c := &Client{ - baseURL: apiBaseURL, - httpClient: httpClient, - appID: appID, + baseURL: apiBaseURL, + httpClient: httpClient, + appID: appID, + issuer: oidcIssuer, + thumbprints: oidcThumbprints, } if privateKey != nil { diff --git a/provider/github-app-token/github/parse_id_token.go b/provider/github-app-token/github/parse_id_token.go new file mode 100644 index 00000000..30d1b8db --- /dev/null +++ b/provider/github-app-token/github/parse_id_token.go @@ -0,0 +1,202 @@ +package github + +import ( + "context" + "crypto/rsa" + "crypto/sha1" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "math/big" + "net/http" + "strings" + + "github.com/golang-jwt/jwt/v4" +) + +type ActionsIDToken struct { + jwt.StandardClaims + Ref string `json:"ref"` + SHA string `json:"sha"` + Repository string `json:"repository"` + RepositoryOwner string `json:"repository_owner"` + RunID string `json:"run_id"` + RunNumber string `json:"run_number"` + RunAttempt string `json:"run_attempt"` + Actor string `json:"actor"` + Workflow string `json:"workflow"` + HeadRef string `json:"head_ref"` + BaseRef string `json:"base_ref"` + EventName string `json:"event_name"` + EventType string `json:"branch"` + JobWorkflowRef string `json:"job_workflow_ref"` + Environment string `json:"environment"` +} + +type openIDConfiguration struct { + Issuer string `json:"issuer"` + JWKSURI string `json:"jwks_uri"` + SubjectTypesSupported []string `json:"subject_types_supported"` + ResponseTypesSupported []string `json:"response_types_supported"` + ClaimsSupported []string `json:"claims_supported"` + IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` + ScopesSupported []string `json:"scopes_supported"` +} + +type jwkSet struct { + Keys []*jwkParams `json:"keys"` +} + +type jwkParams struct { + ID string `json:"kid"` + KeyType string `json:"kty"` + Algorithm string `json:"alg"` + Use string `json:"use,omitempty"` + + X509CertificateChain [][]byte `json:"x5c,omitempty"` + X509CertificateSHA1 string `json:"x5t,omitempty"` + + N string `json:"n,omitempty"` + E string `json:"e,omitempty"` +} + +func (c *Client) ParseIDToken(ctx context.Context, idToken string) (*ActionsIDToken, error) { + var claims ActionsIDToken + _, err := jwt.ParseWithClaims(idToken, &claims, func(token *jwt.Token) (interface{}, error) { + return c.findOIDCKey(ctx, token) + }) + if err != nil { + return nil, err + } + return &claims, nil +} + +func (c *Client) findOIDCKey(ctx context.Context, token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %s", token.Method.Alg()) + } + claims := token.Claims.(*ActionsIDToken) + if claims.Issuer != c.issuer { + return nil, fmt.Errorf("unexpected issuer: %q", claims.Issuer) + } + config, err := c.getOpenIDConfiguration(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get open id configuration: %w", err) + } + keys, err := c.getJWKS(ctx, config.JWKSURI) + if err != nil { + return nil, fmt.Errorf("failed to get jwks: %w", err) + } + + kid, ok := token.Header["kid"].(string) + if !ok { + return nil, errors.New("kid is not found in the jwt header") + } + key, ok := keys[kid] + if !ok { + return nil, fmt.Errorf("key is not found: %q", kid) + } + return key, nil +} + +func (c *Client) getOpenIDConfiguration(ctx context.Context) (*openIDConfiguration, error) { + // TODO: need to cache? + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.issuer+"/.well-known/openid-configuration", nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", githubUserAgent) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, newErrUnexpectedStatusCode(resp) + } + + var config openIDConfiguration + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(&config); err != nil { + return nil, err + } + return &config, nil +} + +func (c *Client) getJWKS(ctx context.Context, url string) (map[string]*rsa.PublicKey, error) { + // TODO: need to cache? + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", githubUserAgent) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // verify the certificate + if resp.TLS == nil { + return nil, errors.New("getting jwks must use encrypted") + } + if certs := resp.TLS.PeerCertificates; len(certs) > 0 { + cert := certs[len(certs)-1] + sum := sha1.Sum(cert.Raw) + thumbprint := hex.EncodeToString(sum[:]) + found := false + for _, want := range c.thumbprints { + if strings.EqualFold(thumbprint, want) { + found = true + break + } + } + if !found { + return nil, errors.New("got invalid certificate during getting jwks") + } + } else { + return nil, errors.New("getting jwks must use encrypted") + } + + if resp.StatusCode != http.StatusOK { + return nil, newErrUnexpectedStatusCode(resp) + } + + var keys jwkSet + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(&keys); err != nil { + return nil, err + } + + result := map[string]*rsa.PublicKey{} + for _, key := range keys.Keys { + if key.KeyType != "RSA" { + // TODO: support other key types? + continue + } + e, err := base64.RawURLEncoding.DecodeString(key.E) + if err != nil { + return nil, fmt.Errorf("failed to parse e param in jwks: %w", err) + } + var ev int + for _, v := range e { + ev = (ev << 8) | int(v) + } + n, err := base64.RawURLEncoding.DecodeString(key.N) + if err != nil { + return nil, fmt.Errorf("failed to parse n param in jwks: %w", err) + } + var nv big.Int + nv.SetBytes(n) + result[key.ID] = &rsa.PublicKey{ + E: ev, + N: &nv, + } + } + return result, nil +} diff --git a/provider/github-app-token/github/parse_id_token_test.go b/provider/github-app-token/github/parse_id_token_test.go new file mode 100644 index 00000000..a1a76098 --- /dev/null +++ b/provider/github-app-token/github/parse_id_token_test.go @@ -0,0 +1,80 @@ +package github + +import ( + "context" + "encoding/json" + "net/http" + "os" + "testing" + "time" +) + +func TestParseIDToken_Intergrated(t *testing.T) { + idToken := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") + idURL := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL") + if idToken == "" || idURL == "" { + t.Skip("it is not in GitHub Actions Environment") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + t.Logf("the request started at %s", time.Now()) + token, err := getIdToken(ctx, idToken, idURL) + if err != nil { + t.Fatal(err) + } + t.Logf("the id is issued at %s", time.Now()) + + // The clock of the token vendor is drifted from the GitHub Actions' runners. + time.Sleep(5 * time.Second) + + c := &Client{ + baseURL: apiBaseURL, + httpClient: http.DefaultClient, + issuer: oidcIssuer, + thumbprints: oidcThumbprints, + } + id, err := c.ParseIDToken(ctx, token) + if err != nil { + t.Fatal(err) + } + t.Logf("sub: %s", id.Subject) + t.Logf("job_workflow_ref: %s", id.JobWorkflowRef) + t.Logf("aud: %s", id.Audience) + t.Logf("issued at %s", time.Unix(id.IssuedAt, 0)) + t.Logf("not before %s", time.Unix(id.NotBefore, 0)) + t.Logf("expires at %s", time.Unix(id.ExpiresAt, 0)) + + if got, want := id.Actor, os.Getenv("GITHUB_ACTOR"); got != want { + t.Errorf("unexpected actor: want %q, got %q", want, got) + } + if got, want := id.Repository, os.Getenv("GITHUB_REPOSITORY"); got != want { + t.Errorf("unexpected repository: want %q, got %q", want, got) + } + if got, want := id.EventName, os.Getenv("GITHUB_EVENT_NAME"); got != want { + t.Errorf("unexpected repository: want %q, got %q", want, got) + } +} + +func getIdToken(ctx context.Context, idToken, idURL string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, idURL, nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer "+idToken) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + var result struct { + Value string `json:"value"` + } + dec := json.NewDecoder(resp.Body) + if err := dec.Decode(&result); err != nil { + return "", err + } + return result.Value, nil +} diff --git a/workspace.code-workspace b/workspace.code-workspace index b57f9812..91c2e1cb 100644 --- a/workspace.code-workspace +++ b/workspace.code-workspace @@ -19,6 +19,10 @@ "**/CVS": true, "**/.DS_Store": true, "github-app-token": true - } + }, + "cSpell.words": [ + "jwks", + "JWKSURI" + ] } }