Skip to content

Commit

Permalink
Merge pull request #29 from shogo82148/support-oidc
Browse files Browse the repository at this point in the history
introduce id token
  • Loading branch information
shogo82148 authored Sep 20, 2021
2 parents 2adab46 + fc71890 commit f034b89
Show file tree
Hide file tree
Showing 13 changed files with 427 additions and 54 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions action/__test__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ describe("tests", () => {
await index.assumeRole({
githubToken: "ghs_dummyGitHubToken",
providerEndpoint: "http://localhost:8080",
audience: "",
});
expect(core.setSecret).toHaveBeenCalledWith("ghs_dummyGitHubToken");
});
Expand Down
20 changes: 13 additions & 7 deletions action/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion action/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
30 changes: 24 additions & 6 deletions action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<GetTokenResult | GetTokenError>(params.providerEndpoint, payload);
if (result.statusCode !== 200) {
const result = await client.postJson<GetTokenResult | GetTokenError>(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;
Expand All @@ -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 = {
Expand All @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions provider/github-app-token/dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package githubapptoken

import (
"context"
"errors"
"net/http"

"github.com/shogo82148/actions-github-app-token/provider/github-app-token/github"
Expand Down Expand Up @@ -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{},
Expand Down
75 changes: 54 additions & 21 deletions provider/github-app-token/github-app-token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -173,15 +190,15 @@ 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 {
return nil, fmt.Errorf("failed create access token: %w", err)
}

return &responseBody{
GitHubToken: token.Token,
GitHubToken: resp.Token,
}, nil
}

Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
26 changes: 14 additions & 12 deletions provider/github-app-token/github-app-token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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{
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
Loading

0 comments on commit f034b89

Please sign in to comment.