Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,22 @@ Create a new job:
- URL: use the hubauth-int URL: `<URL>/cron`


## Enabling Biscuit

To use biscuit tokens instead of bearers, configure the following:

### In Security > Secret manager

Create new secret
- HUBAUTH_BISCUIT_ROOT_PRIVKEY: a base64 encoded p256 EC private key

### In variables

Add a new variable
- TOKEN_TYPE: `Biscuit`
- BISCUIT_ROOT_PRIVKEY: set to the resource ID from `HUBAUTH_BISCUIT_ROOT_PRIVKEY`


## Hubauth CLI

Configure gcloud auth application-default with the following command, and follow the browser instructions:
Expand Down
33 changes: 30 additions & 3 deletions cmd/hubauth-ext/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/flynn/hubauth/pkg/datastore"
"github.com/flynn/hubauth/pkg/httpapi"
"github.com/flynn/hubauth/pkg/idp"
"github.com/flynn/hubauth/pkg/idp/token"
"github.com/flynn/hubauth/pkg/kmssign"
"github.com/flynn/hubauth/pkg/rp/google"
"go.opencensus.io/plugin/ochttp"
Expand Down Expand Up @@ -65,7 +66,33 @@ func main() {
if err != nil {
log.Fatalf("failed to access secret version for %s: %s", name, err)
}
return result.Payload.String()

// Payload.String() would return a json encoded version of the secret: {"data": "..."}
// the actual secret is in Data.
return string(result.Payload.Data)
}

audienceKeyNamer := kmssign.AudienceKeyNameFunc(os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING"))

var accessTokenBuilder token.AccessTokenBuilder
var rootPubKey []byte
tokenType, exists := os.LookupEnv("TOKEN_TYPE")
if !exists {
tokenType = "Bearer"
}
switch tokenType {
case "Bearer":
accessTokenBuilder = token.NewBearerBuilder(kmsClient, audienceKeyNamer)
case "Biscuit":
biscuitKey, err := token.DecodeB64PrivateKey(secret("BISCUIT_ROOT_PRIVKEY"))
if err != nil {
log.Fatalf("failed to initialize biscuit keypair: %v", err)
}

rootPubKey = biscuitKey.Public().Bytes()
accessTokenBuilder = token.NewBiscuitBuilder(kmsClient, audienceKeyNamer, biscuitKey)
default:
log.Fatalf("invalid TOKEN_TYPE, must be one of: Bearer, Biscuit")
}

log.Fatal(http.ListenAndServe(":"+httpPort, &ochttp.Handler{
Expand All @@ -77,15 +104,15 @@ func main() {
os.Getenv("RP_GOOGLE_CLIENT_SECRET"),
os.Getenv("BASE_URL")+"/rp/google",
),
kmsClient,
[]byte(secret("CODE_KEY_SECRET")),
refreshKey,
idp.AudienceKeyNameFunc(os.Getenv("PROJECT_ID"), os.Getenv("KMS_LOCATION"), os.Getenv("KMS_KEYRING")),
accessTokenBuilder,
),
CookieKey: []byte(secret("COOKIE_KEY_SECRET")),
ProjectID: os.Getenv("PROJECT_ID"),
Repository: fmt.Sprintf("https://source.developers.google.com/p/%s/r/%s", os.Getenv("PROJECT_ID"), os.Getenv("BUILD_REPO")),
Revision: os.Getenv("BUILD_REV"),
PublicKey: rootPubKey,
}),
},
))
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/alecthomas/kong v0.2.12
github.com/aws/aws-sdk-go v1.36.7 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/flynn/biscuit-go v0.0.0-20201009174859-e7eb59a90195
github.com/golang/protobuf v1.4.3
github.com/googleapis/gax-go/v2 v2.0.5
github.com/jedib0t/go-pretty/v6 v6.0.5
Expand Down
88 changes: 6 additions & 82 deletions go.sum

Large diffs are not rendered by default.

36 changes: 31 additions & 5 deletions pkg/httpapi/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func (clockImpl) Now() time.Time {
type Config struct {
IdP hubauth.IdPService
CookieKey hmacpb.Key
PublicKey []byte
ProjectID string
Repository string
Revision string
Expand Down Expand Up @@ -87,6 +88,8 @@ func (a *api) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", "GET")
w.Header().Set("Access-Control-Max-Age", "86400")
w.WriteHeader(http.StatusOK)
case req.Method == "GET" && req.URL.Path == "/public-key":
a.PublicKey(w, req)
case req.Method == "GET" && req.URL.Path == "/":
http.Redirect(w, req, "https://flynn.io/", http.StatusFound)
case req.Method == "GET" && req.URL.Path == "/privacy":
Expand Down Expand Up @@ -329,11 +332,12 @@ func (a *api) Token(w http.ResponseWriter, req *http.Request) {
switch req.Form.Get("grant_type") {
case "authorization_code":
res, err = a.IdP.ExchangeCode(req.Context(), &hubauth.ExchangeCodeRequest{
ClientID: req.PostForm.Get("client_id"),
Audience: aud,
RedirectURI: req.PostForm.Get("redirect_uri"),
Code: req.PostForm.Get("code"),
CodeVerifier: req.PostForm.Get("code_verifier"),
ClientID: req.PostForm.Get("client_id"),
Audience: aud,
RedirectURI: req.PostForm.Get("redirect_uri"),
Code: req.PostForm.Get("code"),
CodeVerifier: req.PostForm.Get("code_verifier"),
UserPublicKey: req.PostForm.Get("user_public_key"),
})
case "refresh_token":
res, err = a.IdP.RefreshToken(req.Context(), &hubauth.RefreshTokenRequest{
Expand Down Expand Up @@ -393,6 +397,28 @@ func (a *api) Audiences(w http.ResponseWriter, req *http.Request) {
json.NewEncoder(w).Encode(res)
}

func (a *api) PublicKey(w http.ResponseWriter, req *http.Request) {
if len(a.Config.PublicKey) == 0 {
a.handleErr(w, req, &hubauth.OAuthError{
Code: "unsupported_request",
Description: "no public key configured",
})
return
}

w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")

w.WriteHeader(http.StatusOK)

type key struct {
PublicKey []byte `json:"public-key"`
}
json.NewEncoder(w).Encode(&key{
PublicKey: a.Config.PublicKey,
})
}

func (a *api) handleErr(w http.ResponseWriter, req *http.Request, err error) {
oe, ok := err.(*hubauth.OAuthError)
if !ok {
Expand Down
18 changes: 10 additions & 8 deletions pkg/hubauth/idp.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ type AuthorizeResponse struct {
}

type ExchangeCodeRequest struct {
ClientID string
RedirectURI string
Audience string
Code string
CodeVerifier string
ClientID string
RedirectURI string
Audience string
Code string
CodeVerifier string
UserPublicKey string
}

type AccessToken struct {
Expand All @@ -61,9 +62,10 @@ type AccessToken struct {
}

type RefreshTokenRequest struct {
ClientID string
Audience string
RefreshToken string
ClientID string
Audience string
RefreshToken string
UserPublicKey string
}

type ListAudiencesRequest struct {
Expand Down
97 changes: 53 additions & 44 deletions pkg/idp/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ package idp

import (
"context"
"crypto"
"encoding/base64"
"net/url"
"strings"
"time"

"github.com/flynn/hubauth/pkg/clog"
"github.com/flynn/hubauth/pkg/hmacpb"
"github.com/flynn/hubauth/pkg/hubauth"
"github.com/flynn/hubauth/pkg/kmssign"
"github.com/flynn/hubauth/pkg/idp/token"
"github.com/flynn/hubauth/pkg/pb"
"github.com/flynn/hubauth/pkg/rp"
"github.com/flynn/hubauth/pkg/signpb"
Expand All @@ -22,22 +20,10 @@ import (
"golang.org/x/sync/errgroup"
)

type AudienceKeyNamer func(audience string) string

const oobRedirectURI = "urn:ietf:wg:oauth:2.0:oob"
const codeExpiry = 30 * time.Second
const accessTokenDuration = 5 * time.Minute

func AudienceKeyNameFunc(projectID, location, keyRing string) func(string) string {
return func(aud string) string {
u, err := url.Parse(aud)
if err != nil {
return ""
}
return fmt.Sprintf("projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s/cryptoKeyVersions/1", projectID, location, keyRing, strings.Replace(u.Host, ".", "_", -1))
}
}

type clock interface {
Now() time.Time
}
Expand All @@ -61,34 +47,31 @@ type idpSteps interface {
SignRefreshToken(ctx context.Context, signKey signpb.PrivateKey, t *signedRefreshTokenData) (string, error)
RenewRefreshToken(ctx context.Context, clientID, oldTokenID string, oldTokenIssueTime, now time.Time) (*hubauth.RefreshToken, error)
VerifyRefreshToken(ctx context.Context, rt *hubauth.RefreshToken, now time.Time) error
SignAccessToken(ctx context.Context, signKey signpb.PrivateKey, t *accessTokenData, now time.Time) (string, error)
BuildAccessToken(ctx context.Context, audience string, t *token.AccessTokenData) (string, string, error)
}

type idpService struct {
db hubauth.DataStore
rp rp.AuthService
kms kmssign.KMSClient
db hubauth.DataStore
rp rp.AuthService

codeKey hmacpb.Key
refreshKey signpb.Key
audienceKey AudienceKeyNamer
codeKey hmacpb.Key
refreshKey signpb.Key

steps idpSteps
clock clock
}

var _ hubauth.IdPService = (*idpService)(nil)

func New(db hubauth.DataStore, rp rp.AuthService, kms kmssign.KMSClient, codeKey hmacpb.Key, refreshKey signpb.Key, audienceKey AudienceKeyNamer) hubauth.IdPService {
func New(db hubauth.DataStore, rp rp.AuthService, codeKey hmacpb.Key, refreshKey signpb.Key, tokenBuilder token.AccessTokenBuilder) hubauth.IdPService {
return &idpService{
db: db,
rp: rp,
kms: kms,
codeKey: codeKey,
refreshKey: refreshKey,
audienceKey: audienceKey,
db: db,
rp: rp,
codeKey: codeKey,
refreshKey: refreshKey,
steps: &steps{
db: db,
db: db,
builder: tokenBuilder,
},
clock: clockImpl{},
}
Expand Down Expand Up @@ -321,16 +304,29 @@ func (s *idpService) ExchangeCode(parentCtx context.Context, req *hubauth.Exchan
})

var accessToken string
var tokenType string
g.Go(func() (err error) {
if req.Audience == "" {
return nil
}
signKey := kmssign.NewPrivateKey(s.kms, s.audienceKey(req.Audience), crypto.SHA256)
accessToken, err = s.steps.SignAccessToken(ctx, signKey, &accessTokenData{
clientID: req.ClientID,
userID: codeInfo.UserId,
userEmail: codeInfo.UserEmail,
}, now)

var userPublicKey []byte
if len(req.UserPublicKey) > 0 {
var err error
userPublicKey, err = base64Decode(req.UserPublicKey)
if err != nil {
return fmt.Errorf("idp: invalid public key: %v", err)
}
}

accessToken, tokenType, err = s.steps.BuildAccessToken(ctx, req.Audience, &token.AccessTokenData{
ClientID: req.ClientID,
UserID: codeInfo.UserId,
UserEmail: codeInfo.UserEmail,
UserPublicKey: userPublicKey,
IssueTime: now,
ExpireTime: now.Add(accessTokenDuration),
})
return err
})

Expand All @@ -353,7 +349,7 @@ func (s *idpService) ExchangeCode(parentCtx context.Context, req *hubauth.Exchan
res.AccessToken = res.RefreshToken
res.ExpiresIn = res.RefreshTokenExpiresIn
} else {
res.TokenType = "Bearer"
res.TokenType = tokenType
res.ExpiresIn = int(accessTokenDuration / time.Second)
}
return res, nil
Expand Down Expand Up @@ -395,16 +391,29 @@ func (s *idpService) RefreshToken(ctx context.Context, req *hubauth.RefreshToken
})

var accessToken string
var tokenType string
g.Go(func() (err error) {
if req.Audience == "" {
return nil
}
signKey := kmssign.NewPrivateKey(s.kms, s.audienceKey(req.Audience), crypto.SHA256)
accessToken, err = s.steps.SignAccessToken(ctx, signKey, &accessTokenData{
clientID: req.ClientID,
userID: oldToken.UserID,
userEmail: oldToken.UserEmail,
}, now)

var userPublicKey []byte
if len(req.UserPublicKey) > 0 {
var err error
userPublicKey, err = base64Decode(req.UserPublicKey)
if err != nil {
return fmt.Errorf("idp: invalid public key: %v", err)
}
}

accessToken, tokenType, err = s.steps.BuildAccessToken(ctx, req.Audience, &token.AccessTokenData{
ClientID: req.ClientID,
UserID: oldToken.UserID,
UserEmail: oldToken.UserEmail,
UserPublicKey: userPublicKey,
IssueTime: now,
ExpireTime: now.Add(accessTokenDuration),
})
return err
})

Expand All @@ -426,7 +435,7 @@ func (s *idpService) RefreshToken(ctx context.Context, req *hubauth.RefreshToken
res.AccessToken = res.RefreshToken
res.ExpiresIn = res.RefreshTokenExpiresIn
} else {
res.TokenType = "Bearer"
res.TokenType = tokenType
res.ExpiresIn = int(accessTokenDuration / time.Second)
}
return res, nil
Expand Down
Loading