githubapp

package module
v0.0.0-...-e43486a Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 4, 2024 License: MIT Imports: 26 Imported by: 2

README

go-githubapp

go-reference go-version license test lint release version

HTTP Round Tripper to authenticate to GitHub as GitHub app and utilities for WebHook Verification. Supports authenticating with Installation Token and JWT.

Example

// SPDX-FileCopyrightText: Copyright 2024 Prasad Tengse
// SPDX-License-Identifier: MIT

package main

import (
    "log"
    "net/http"
    "github.com/tprasadtp/go-githubapp"
)

func main() {
	transport, err := githubapp.NewTransport(ctx, appID, signer,
        githubapp.WithOwner("username"),
        githubapp.WithRepositories("repository"),
        githubapp.WithPermissions("contents:read"),
    )

    // Build an HTTP client with custom round tripper.
    client := &http.Client{
        Transport: transport,
    }

    // Try to fetch README for the repository.
    response, err := client.Get("/repos/<username>/<repository>/readme")

    // Handle error
    if err != nil {
        log.Fatal(err)
    }

    // Process Response from API....
}

API Reference

  • This library is designed to provide automatic authentication for google/go-github, github.com/shurcooL/githubv4 or your own HTTP client.
  • Transport implements http.RoundTripper which can authenticate transparently. It will override Authorization header. None of the other headers are modified. It is user's responsibility to set appropriate headers (like user agent etc.) as required.

See API docs for more info and examples.

AppID

App ID can be found at

Settings -> Developer -> settings -> GitHub App -> About item.

Be sure to select the correct organization if you are a member of multiple organizations.

Private Key

This library delegates JWT signing to type implementing crypto.Signer interface. Thus, it may be backed by KMS/TPM or other secure key store. Optionally github.com/tprasadtp/cryptokms can be used.

Installation ID

Typically extracted from webhook request headers. If using VerifyWebHookRequest, returned WebHook includes InstallationID. This is not required if an owner is already specified.

Limit Permissions of Tokens

WithPermissions can be used to limit permissions on the created tokens. WithPermissions accepts permissions in <scope>:<level> format. Please check with GitHub API documentation on supported scopes. Requested permissions cannot permissions existing on the installation.

Limit the Scope of Tokens to a set of Repositories

WithRepositories can be used to limit the scope of created access tokens to the list of repositories specified. Repositories MUST belong to a single installation i.e., MUST have a single owner. This accepts repositories in {owner}/{repo} format or just name of the repository. If only name is specified, then it MUST be used with WithOwner or WithInstallationID.

Using GitHub Enterprise Server

WithEndpoint can be used to use custom GitHub REST endpoint. This endpoint will ONLY be used for token renewals and verifying app installation and not http client using the Transport.

Authenticating as an App (JWT)

When none of the installation options WithOwner, WithInstallationID or WithRepositories are specified, Transport authenticates as an app. Some API endpoints like listing installations are only accessible to app.

Verifying Webhooks

VerifyWebHookRequest provides a way to verify webhook payload and extract event data from headers. See API docs for more info.

Documentation

Overview

Package githubapp is client middleware to authenticate to GitHub as an app.

Index

Constants

View Source
const (
	// ErrWebHookMethod is returned by [VerifyWebHookRequest] when a request method
	// is not PUT.
	ErrWebHookMethod = Error("githubapp(webhook): method not supported")

	// ErrWebHookContentType is returned by [VerifyWebHookRequest] when a request
	// content type is not 'application/json'.
	ErrWebHookContentType = Error("githubapp(webhook): unsupported content type")

	// ErrWebHookRequest is returned by [VerifyWebHookRequest] when request is invalid
	// or missing GitHub specific webhook metadata headers (X-GitHub-Event, X-GitHub-Hook-ID etc.).
	ErrWebHookRequest = Error("githubapp(webhook): invalid request")

	// ErrWebhookSignature is returned by [VerifyWebHookRequest] when the signature does not match.
	ErrWebhookSignature = Error("githubapp(webhook): HMAC-SHA256 signature is invalid")
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Error

type Error string

Error is immutable error representation.

Error strings themselves are NOT part of semver compatibility guarantees. Use exported symbols instead of directly using error strings.

func (Error) Error

func (e Error) Error() string

Implements Error() interface.

type InstallationToken

type InstallationToken struct {
	// Installation access token. Typically starts with "ghs_".
	Token string `json:"token,omitempty" yaml:"token,omitempty"`

	// GitHub app ID.
	AppID uint64 `json:"app_id,omitempty" yaml:"appID,omitempty"`

	// GitHub app name.
	AppName string `json:"app_name,omitempty" yaml:"appName,omitempty"`

	// Installation ID for the app.
	InstallationID uint64 `json:"installation_id,omitempty" yaml:"installationID,omitempty"`

	// GitHub API endpoint. This is also used for token revocation.
	// If omitted, assume the default value of "https://api.githhub.com/".
	Server string `json:"server,omitempty" yaml:"server,omitempty"`

	// UserAgent used to fetch this installation access token.
	UserAgent string `json:"user_agent,omitempty" yaml:"user_agent,omitempty"`

	// Token exp time.
	Exp time.Time `json:"exp,omitempty" yaml:"exp,omitempty"`

	// Installation owner. This is owner of the installation.
	Owner string `json:"owner,omitempty" yaml:"owner,omitempty"`

	// Repositories which can be accessed with the token. This may be empty
	//  if a scoped token is not requested. In such cases, token will have access to all
	// repositories accessible by the installation.
	Repositories []string `json:"repositories,omitempty" yaml:"repositories,omitempty"`

	// Permissions available for the token. This may be omitted if scoped permissions are not
	// requested. In such cases token has all permissions available to the installation.
	Permissions map[string]string `json:"permissions,omitempty" yaml:"permissions,omitempty"`

	// BotUsername is app's github username.
	BotUsername string `json:"bot_username,omitempty" yaml:"bot_username,omitempty"`

	// BotCommitterEmail is committer email to use to attribute commits to the bot.
	// This is in the form "<user-id>+<app-name>[bot]@users.noreply.github.com".
	BotCommitterEmail string `json:"bot_committer_email,omitempty" yaml:"bot_committer_email,omitempty"`
}

InstallationToken is an installation access token from GitHub.

func NewInstallationToken

func NewInstallationToken(ctx context.Context, appid uint64, signer crypto.Signer, opts ...Option) (InstallationToken, error)

NewInstallationToken returns new installation access token. This takes same options as Transport.

func (*InstallationToken) IsValid

func (t *InstallationToken) IsValid() bool

IsValid checks if InstallationToken is valid for at-least 60 seconds.

func (*InstallationToken) LogValue

func (t *InstallationToken) LogValue() slog.Value

LogValue implements log/slog.LogValuer.

func (*InstallationToken) Revoke

func (t *InstallationToken) Revoke(ctx context.Context) error

Revoke revokes the installation access token.

type JWT

type JWT struct {
	// JWT token.
	Token string `json:"token" yaml:"token"`

	// GitHub app ID.
	AppID uint64 `json:"app_id,omitempty" yaml:"appID,omitempty"`

	// GitHub app name.
	AppName string `json:"app_name,omitempty" yaml:"appName,omitempty"`

	// Token exp time.
	Exp time.Time `json:"exp,omitempty" yaml:"exp,omitempty"`

	// Token issue time.
	IssuedAt time.Time `json:"iat,omitempty" yaml:"iat,omitempty"`
}

JWT is JWT token used to authenticate as app.

func NewJWT

func NewJWT(ctx context.Context, appid uint64, signer crypto.Signer) (JWT, error)

NewJWT returns new JWT bearer token signed by the signer.

Returned JWT is valid for at least 5min. Ensure that your machine's clock is accurate.

  • Unlike NewTransport, this does not validate app id and signer. This simply mints the JWT as required by GitHub app authentication.
  • RSA keys of length less than 2048 bits are not supported.
  • Only RSA keys are supported. Using ECDSA, ED25519 or other keys will return error.

func (JWT) IsValid

func (t JWT) IsValid() bool

IsValid checks if JWT is valid for at-least 60 seconds.

func (JWT) LogValue

func (t JWT) LogValue() slog.Value

LogValue implements log/slog.LogValuer.

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option is option to apply for Transport.

func Options

func Options(options ...Option) Option

Options takes a variadic slice of Options and returns a single Options which includes all the given options. This is useful for sharing presets. If conflicting options are specified, last-specified wins. As a special case, if no options are specified or all specified options are nil, this will returns nil.

func WithEndpoint

func WithEndpoint(endpoint string) Option

WithEndpoint configures Transport to use custom REST API(v3) endpoint. for authenticating as app, obtaining installation metadata and creating installation access tokens. This MUST be REST(v3) endpoint even though a client might be using GitHub GraphQL API.

When not specified or empty, "https://api.github.com/" is used.

func WithInstallationID

func WithInstallationID(id uint64) Option

WithInstallationID configures Transport to use installation id specified.

This is useful if it is required to access all repositories available for an installation without specifying them individually or if building Transport from data provided by WebHook.

func WithOwner

func WithOwner(username string) Option

WithOwner configures the installation owner to use.

func WithPermissions

func WithPermissions(permissions ...string) Option

WithPermissions configures permission scopes. This is useful when app has a broader set of permissions, a scoped access token is required.

Permissions MUST be specified in "<scope>:<access>" or "<scope>=<access>" format. Where scope is permission scope like "issues" and access can be one of "read", "write" or "admin".

For example, to request permissions to write issues and pull request can be specified as,

githubapp.WithPermissions("issues:write", "pull_requests:write")

func WithRepositories

func WithRepositories(repos ...string) Option

WithRepositories configures Transport to use installation for repos specified. Unlike other installation options, this can be used multiple times.

func WithRoundTripper

func WithRoundTripper(next http.RoundTripper) Option

WithRoundTripper configures Transport to use next as next http.RoundTripper.

This can be used to further customize headers, add logging or retries. This only applies to authentication API calls and not the http client using the Transport.

func WithUserAgent

func WithUserAgent(ua string) Option

WithUserAgent configures user agent header to use for token related API requests.

Typically, Transport which implements http.RoundTripper will re-use the User-Agent header specified by the http.Request. However, when building the Transport several HTTP requests need to be made to verify and configure it. User agent specified here will be used during bootstrapping. This is also as fallback for token renewal requests.

type Transport

type Transport struct {
	// contains filtered or unexported fields
}

Transport provides a http.RoundTripper by wrapping an existing http.RoundTripper and provides GitHub Apps authenticating as a GitHub App or as an GitHub app installation.

'Authorization' header is automatically populated with a suitable installation token or JWT token for all requests. If it already exists, it is ignored. Token renewal requests will always override 'Accept' and "X-GitHub-Api-Version" headers.

func NewTransport

func NewTransport(ctx context.Context, appid uint64, signer crypto.Signer, opts ...Option) (*Transport, error)

NewTransport creates a new Transport for authenticating as an app/installation.

How Transport authenticates depends on installation options specified.

  • If no installation options are specified, then Transport can only authenticate as app (using JWT). This is not something you want typically, as a very limited number of actions like accessing available installations.
  • Use WithInstallationID to have access to all permissions available to the installation including organization scopes and repositories. This can be used together with WithPermissions to limit the scope of access tokens. A typical example would be to close all stale issues for all repositories in an organization. This task does not require access to code, thus "issues:write" permission should be sufficient.
  • Use WithOwner if your app has only access to organization/user permissions and none of the repositories belonging to the owner. A typical example would be an app, which manages self-hosted runners in an organization or manages organization level projects.
  • Use WithRepositories if your app intends to access only a set of repositories. Do note that if app has access to organization permissions, they will also be available to the access token, unless limited with WithPermissions.
  • WithPermissions can be used to limit the scope of permissions available to the access token.

Access token and JWT are automatically refreshed whenever required.

If only installation access token or JWT is required but not the round tripper, use NewInstallationToken or NewJWT respectively.

func (*Transport) AppID

func (t *Transport) AppID() uint64

AppID returns the GitHub app id.

func (*Transport) AppName

func (t *Transport) AppName() string

AppName returns the GitHub app slug.

func (*Transport) BotCommitterEmail

func (t *Transport) BotCommitterEmail() string

BotCommitterEmail returns the GitHub app's no-reply email to use for git metadata.

func (*Transport) BotUsername

func (t *Transport) BotUsername() string

BotUsername returns the GitHub app's username.

func (*Transport) InstallationID

func (t *Transport) InstallationID() uint64

InstallationID returns the GitHub installation id. If not repositories or organizations are configured, This will return 0.

func (*Transport) InstallationToken

func (t *Transport) InstallationToken(ctx context.Context) (InstallationToken, error)

InstallationToken returns a new installation access token. This always returns a new token, thus callers can safely revoke the token whenever required.

func (*Transport) JWT

func (t *Transport) JWT(ctx context.Context) (JWT, error)

JWT returns already existing JWT bearer token or mints a new one.

func (*Transport) RoundTrip

func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error)

func (*Transport) ScopedPermissions

func (t *Transport) ScopedPermissions() map[string]string

ScopedPermissions returns permissions configured for the transport. This is not the same as app permissions. This will return nil if no scoped permissions are set.

type WebHook

type WebHook struct {
	// ID is webhook ID received in X-GitHub-Hook-ID header.
	ID string

	// Event is event type like "issues" received in X-GitHub-Event header.
	Event string

	// Payload is payload received in POST.
	Payload []byte

	// DeliveryID is a unique delivery id received in X-GitHub-DeliveryID header.
	DeliveryID string

	// Signature is HMAC hex digest of the request body with the prefix "sha256=".
	// This is populated from X-Hub-Signature-256 header.
	Signature string

	// GitHub app installation ID. This can be used by WithInstallationID
	// for building Transport applicable for the installation in the hook event.
	InstallationID uint64

	// InstallationType can be repo|user|org.
	InstallationType string
}

WebHook is returned by VerifyWebHookRequest upon successful verification of the webhook request. It contains all the webhook payloads with additional info from headers to detect GitHub app installation.

func VerifyWebHookRequest

func VerifyWebHookRequest(secret string, req *http.Request) (WebHook, error)

VerifyWebHookRequest is a simple function to verify webhook HMAC-SHA256 signature.

This functions assumes that headers are canonical by default and have not been modified. Only HMAC-SHA256 signatures are considered for verification and SHA1 signature headers are ignored.

Typically, HMAC secret would be []byte, but as it may be updated via web interface, which can only accept strings. Returned value is only valid if error is nil.

  • ErrWebHookRequest is returned when request is invalid and is missing or malformed headers like 'X-GitHub-Event', 'X-Hub-Signature-256' and more.
  • ErrWebHookMethod is returned when webhook request is not a PUT request.
  • ErrWebHookContentType is returned when content type header is not set to 'application/json'. Though GitHub supports 'application/x-www-form-urlencoded', it is NOT supported by this library.
  • ErrWebhookSignature is returned when signature does not match.

An example HTTP handler which returns appropriate http status code is shown below.

mux.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
    webhook, err := githubapp.VerifyWebHookRequest(secret, r)
    if err != nil {
        switch {
        case errors.Is(err, githubapp.ErrWebhookSignature):
            w.WriteHeader(http.StatusUnauthorized)
        case errors.Is(err, githubapp.ErrWebHookRequest):
            w.WriteHeader(http.StatusBadRequest)
        case errors.Is(err, githubapp.ErrWebHookContentType):
            w.WriteHeader(http.StatusUnsupportedMediaType)
        case errors.Is(err, githubapp.ErrWebHookMethod):
            w.WriteHeader(http.StatusMethodNotAllowed)
        default:
            // This is non-reachable code.
            w.WriteHeader(http.StatusNotImplemented)
        }
        _, _ = w.Write([]byte(err.Error()))
        return
    }

    // Do something with webhook, for example, put it in SQS or PubSub.
    err = doSomething(r.Context(), webhook)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }

	// Return HTTP status 2xx.
    w.WriteHeader(http.StatusAccepted)
})

func (*WebHook) LogValue

func (w *WebHook) LogValue() slog.Value

Directories

Path Synopsis
examples
app-token
An example CLI which can fetch installation tokens for a GitHub app.
An example CLI which can fetch installation tokens for a GitHub app.
go-github Module
internal
api
Package api holds types and methods to serialize and deserialize requests to and from GitHub API.
Package api holds types and methods to serialize and deserialize requests to and from GitHub API.
testkeys
Package testkeys generates ephemeral test keys.
Package testkeys generates ephemeral test keys.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL