Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance(oidc): add more claims #1172

Merged
merged 12 commits into from
Sep 5, 2024
3 changes: 3 additions & 0 deletions api/oi_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func GetOpenIDConfig(c *gin.Context) {
"iat",
"iss",
"aud",
"branch",
"build_number",
"build_id",
"repo",
Expand All @@ -54,6 +55,8 @@ func GetOpenIDConfig(c *gin.Context) {
"actor_scm_id",
"commands",
"image",
"image_name",
"image_tag",
"request",
"event",
"sha",
Expand Down
15 changes: 9 additions & 6 deletions api/types/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,21 @@ type OpenIDConfig struct {
// OpenIDClaims struct is an extension of the JWT standard claims. It
// includes information relevant to OIDC services.
type OpenIDClaims struct {
BuildNumber int `json:"build_number,omitempty"`
BuildID int64 `json:"build_id,omitempty"`
Actor string `json:"actor,omitempty"`
ActorSCMID string `json:"actor_scm_id,omitempty"`
Repo string `json:"repo,omitempty"`
TokenType string `json:"token_type,omitempty"`
Image string `json:"image,omitempty"`
Request string `json:"request,omitempty"`
Branch string `json:"branch,omitempty"`
BuildID int64 `json:"build_id,omitempty"`
BuildNumber int `json:"build_number,omitempty"`
Commands bool `json:"commands,omitempty"`
Event string `json:"event,omitempty"`
Image string `json:"image,omitempty"`
ImageName string `json:"image_name,omitempty"`
ImageTag string `json:"image_tag,omitempty"`
Ref string `json:"ref,omitempty"`
Repo string `json:"repo,omitempty"`
Request string `json:"request,omitempty"`
SHA string `json:"sha,omitempty"`
TokenType string `json:"token_type,omitempty"`
jwt.RegisteredClaims
}

Expand Down
32 changes: 29 additions & 3 deletions internal/token/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"time"

"github.com/golang-jwt/jwt/v5"
Expand Down Expand Up @@ -120,7 +121,7 @@ func (tm *Manager) MintToken(mto *MintTokenOpts) (string, error) {

tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

//sign token with configured private signing key
// sign token with configured private signing key
token, err := tk.SignedString([]byte(tm.PrivateKeyHMAC))
if err != nil {
return "", fmt.Errorf("unable to sign token: %w", err)
Expand All @@ -134,6 +135,8 @@ func (tm *Manager) MintIDToken(ctx context.Context, mto *MintTokenOpts, db datab
// initialize claims struct
var claims = new(api.OpenIDClaims)

var err error

// validate provided claims
if len(mto.Repo) == 0 {
return "", errors.New("missing repo for ID token")
Expand All @@ -154,6 +157,7 @@ func (tm *Manager) MintIDToken(ctx context.Context, mto *MintTokenOpts, db datab
// set claims based on input
claims.Actor = mto.Build.GetSender()
claims.ActorSCMID = mto.Build.GetSenderSCMID()
claims.Branch = mto.Build.GetBranch()
claims.BuildNumber = mto.Build.GetNumber()
claims.BuildID = mto.Build.GetID()
claims.Repo = mto.Repo
Expand All @@ -164,6 +168,12 @@ func (tm *Manager) MintIDToken(ctx context.Context, mto *MintTokenOpts, db datab
claims.Audience = mto.Audience
claims.TokenType = mto.TokenType
claims.Image = mto.Image

claims.ImageName, claims.ImageTag, err = imageParse(mto.Image)
if err != nil {
return "", err
}

claims.Request = mto.Request
claims.Commands = mto.Commands

Expand All @@ -175,7 +185,7 @@ func (tm *Manager) MintIDToken(ctx context.Context, mto *MintTokenOpts, db datab
tk := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)

// verify key is active in the database before signing
_, err := db.GetActiveJWK(ctx, tm.RSAKeySet.KID)
_, err = db.GetActiveJWK(ctx, tm.RSAKeySet.KID)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return "", fmt.Errorf("unable to get active public key: %w", err)
Expand All @@ -191,7 +201,7 @@ func (tm *Manager) MintIDToken(ctx context.Context, mto *MintTokenOpts, db datab
// set KID header
tk.Header["kid"] = tm.RSAKeySet.KID

//sign token with configured private signing key
// sign token with configured private signing key
token, err := tk.SignedString(tm.RSAKeySet.PrivateKey)
if err != nil {
return "", fmt.Errorf("unable to sign token: %w", err)
Expand All @@ -201,3 +211,19 @@ func (tm *Manager) MintIDToken(ctx context.Context, mto *MintTokenOpts, db datab

return token, nil
}

// imageParse parses the given image string and returns the image name and tag.
// If no tag is provided in the image string, "latest" is used as the tag.
// If the image string is invalid, an error is returned.
func imageParse(image string) (string, string, error) {
parts := strings.Split(image, ":")

switch len(parts) {
case 1:
return image, "latest", nil
case 2:
return parts[0], parts[1], nil
default:
return "", "", fmt.Errorf("invalid image format: %s", image)
}
}
61 changes: 61 additions & 0 deletions internal/token/mint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0

package token

import "testing"

func Test_imageParse(t *testing.T) {
type args struct {
image string
}
tests := []struct {
name string
args args
wantName string
wantTag string
wantErr bool
}{
{
name: "image with tag",
args: args{
image: "alpine:1.20",
},
wantName: "alpine",
wantTag: "1.20",
wantErr: false,
},
{
name: "image without latest tag",
args: args{
image: "alpine:latest",
},
wantName: "alpine",
wantTag: "latest",
wantErr: false,
},
{
name: "image without tag",
args: args{
image: "alpine",
},
wantName: "alpine",
wantTag: "latest",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, got1, err := imageParse(tt.args.image)
if (err != nil) != tt.wantErr {
t.Errorf("imageParse() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.wantName {
t.Errorf("imageParse() got = %v, wantName %v", got, tt.wantName)
}
if got1 != tt.wantTag {
t.Errorf("imageParse() got1 = %v, wantName %v", got1, tt.wantTag)
}
})
}
}
3 changes: 3 additions & 0 deletions mock/server/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"iat",
"iss",
"aud",
"branch",
"build_number",
"build_id",
"repo",
Expand All @@ -40,6 +41,8 @@
"actor_scm_id",
"commands",
"image",
"image_name",
"image_tag",
"request"
],
"id_token_signing_alg_values_supported": [
Expand Down Expand Up @@ -78,7 +81,7 @@

state := c.Request.FormValue("state")
code := c.Request.FormValue("code")
err := "error"

Check failure on line 84 in mock/server/authentication.go

View workflow job for this annotation

GitHub Actions / golangci

[golangci] mock/server/authentication.go#L84

string `error` has 4 occurrences, make it a constant (goconst)
Raw output
mock/server/authentication.go:84:9: string `error` has 4 occurrences, make it a constant (goconst)
	err := "error"
	       ^

if len(state) == 0 && len(code) == 0 {
c.AbortWithStatusJSON(http.StatusUnauthorized, types.Error{Message: &err})
Expand Down
Loading