Skip to content
Merged
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
2 changes: 1 addition & 1 deletion app/controlplane/internal/biz/robotaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func (uc *RobotAccountUseCase) Create(ctx context.Context, name string, orgID, w
return nil, err
}

jwt, err := b.GenerateJWT(orgID, workflowID, res.ID.String(), jwt.DefaultAudience)
jwt, err := b.GenerateJWT(orgID, workflowID, res.ID.String())
if err != nil {
return nil, err
}
Expand Down
1 change: 0 additions & 1 deletion app/controlplane/internal/jwt/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@
package jwt

const DefaultIssuer = "cp.chainloop"
const DefaultAudience = "client.chainloop"
const CASAudience = "artifact-cas.chainloop"
11 changes: 9 additions & 2 deletions app/controlplane/internal/jwt/robotaccount/robotaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ import (

var SigningMethod = jwt.SigningMethodHS256

// This type of JWT is meant to be used by the attestations service
const (
Audience = "attestations.chainloop"
// Previous audience, deprecated, we keep it to not to break compatibility
DeprecatedAudience = "client.chainloop"
)

type Builder struct {
issuer string
hmacSecret string
Expand Down Expand Up @@ -64,15 +71,15 @@ func NewBuilder(opts ...NewOpt) (*Builder, error) {
}

// NOTE: It does not expire, it will get revoked instead
func (ra *Builder) GenerateJWT(orgID, workflowID, keyID, audience string) (string, error) {
func (ra *Builder) GenerateJWT(orgID, workflowID, keyID string) (string, error) {
claims := CustomClaims{
orgID,
workflowID,
jwt.RegisteredClaims{
// Key identifier so we can check it's revocation status
ID: keyID,
Issuer: ra.issuer,
Audience: jwt.ClaimStrings{audience},
Audience: jwt.ClaimStrings{Audience},
},
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestGenerateJWT(t *testing.T) {
)
require.NoError(t, err)

token, err := b.GenerateJWT("org-id", "workflow-id", "key-id", "my-audience")
token, err := b.GenerateJWT("org-id", "workflow-id", "key-id")
assert.NoError(t, err)
assert.NotEmpty(t, token)

Expand All @@ -91,6 +91,6 @@ func TestGenerateJWT(t *testing.T) {
assert.Equal(t, "workflow-id", claims.WorkflowID)
assert.Equal(t, "key-id", claims.ID)
assert.Equal(t, "my-issuer", claims.Issuer)
assert.Contains(t, claims.Audience, "my-audience")
assert.Contains(t, claims.Audience, Audience)
assert.Nil(t, claims.ExpiresAt)
}
6 changes: 4 additions & 2 deletions app/controlplane/internal/jwt/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"github.com/golang-jwt/jwt/v4"
)

const Audience = "user-auth.chainloop"

type Builder struct {
issuer string
hmacSecret string
Expand Down Expand Up @@ -71,12 +73,12 @@ func NewBuilder(opts ...NewOpt) (*Builder, error) {
return b, nil
}

func (ra *Builder) GenerateJWT(userID, audience string) (string, error) {
func (ra *Builder) GenerateJWT(userID string) (string, error) {
claims := CustomClaims{
userID,
jwt.RegisteredClaims{
Issuer: ra.issuer,
Audience: jwt.ClaimStrings{audience},
Audience: jwt.ClaimStrings{Audience},
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ra.expiration)),
},
}
Expand Down
4 changes: 2 additions & 2 deletions app/controlplane/internal/jwt/user/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestGenerateJWT(t *testing.T) {
)
require.NoError(t, err)

token, err := b.GenerateJWT("user-id", "my-audience")
token, err := b.GenerateJWT("user-id")
assert.NoError(t, err)
assert.NotEmpty(t, token)

Expand All @@ -91,6 +91,6 @@ func TestGenerateJWT(t *testing.T) {
assert.True(t, tokenInfo.Valid)
assert.Equal(t, "user-id", claims.UserID)
assert.Equal(t, "my-issuer", claims.Issuer)
assert.Contains(t, claims.Audience, "my-audience")
assert.Contains(t, claims.Audience, Audience)
assert.WithinDuration(t, time.Now(), claims.ExpiresAt.Time, 10*time.Second)
}
2 changes: 1 addition & 1 deletion app/controlplane/internal/service/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ func generateUserJWT(userID, passphrase string, expiration time.Duration) (strin
return "", err
}

return b.GenerateJWT(userID, jwt.DefaultAudience)
return b.GenerateJWT(userID)
}

func setOauthCookie(w http.ResponseWriter, name, value string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ func WithCurrentRobotAccount(robotAccountUseCase *biz.RobotAccountUseCase, logge
return nil, errors.New("error mapping the claims")
}

// Do not accept tokens that are crafted for a different audience in this system
// NOTE: we allow deprecated audience to not to break compatibility with previously issued robot-accounts
if !claims.VerifyAudience(robotaccount.Audience, true) && !claims.VerifyAudience(robotaccount.DeprecatedAudience, true) {
return nil, errors.New("unexpected token, invalid audience")
}

// Extract account ID
robotAccountID := claims.ID
if robotAccountID == "" {
Expand Down
5 changes: 5 additions & 0 deletions app/controlplane/internal/usercontext/userorg_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ func WithCurrentUserAndOrgMiddleware(userUseCase biz.UserOrgFinder, logger *log.
return nil, errors.New("error mapping the claims")
}

// Do not accept tokens that are crafted for a different audience in this system
if !customClaims.VerifyAudience(user.Audience, true) {
return nil, errors.New("unexpected token, invalid audience")
}

userID := customClaims.UserID
if userID == "" {
return nil, errors.New("error retrieving the user information from the auth token")
Expand Down
17 changes: 16 additions & 1 deletion app/controlplane/internal/usercontext/userorg_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
userjwtbuilder "github.com/chainloop-dev/chainloop/app/controlplane/internal/jwt/user"
"github.com/go-kratos/kratos/v2/log"
jwtmiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
Expand All @@ -36,32 +37,43 @@ func TestWithCurrentUserAndOrgMiddleware(t *testing.T) {
testCases := []struct {
name string
loggedIn bool
audience string
userExist bool
orgExist bool
wantErr bool
}{
{
name: "invalid audience",
loggedIn: true,
audience: "another-aud",
wantErr: true,
},
{
name: "logged in, user and org exists",
loggedIn: true,
audience: userjwtbuilder.Audience,
userExist: true,
orgExist: true,
wantErr: false,
},
{
name: "logged in, user does not exist",
loggedIn: true,
audience: userjwtbuilder.Audience,
userExist: false,
wantErr: true,
},
{
name: "logged in, org does not exist",
loggedIn: true,
audience: userjwtbuilder.Audience,
userExist: true,
wantErr: true,
},
{
name: "not logged in",
loggedIn: false,
audience: userjwtbuilder.Audience,
wantErr: true,
},
}
Expand All @@ -76,13 +88,16 @@ func TestWithCurrentUserAndOrgMiddleware(t *testing.T) {
if tc.loggedIn {
ctx = jwtmiddleware.NewContext(ctx, &userjwtbuilder.CustomClaims{
UserID: wantUser.ID,
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{tc.audience},
},
})
}

if tc.userExist {
usecase.On("FindByID", ctx, wantUser.ID).Return(wantUser, nil)
} else if tc.loggedIn {
usecase.On("FindByID", ctx, wantUser.ID).Return(nil, nil)
usecase.On("FindByID", ctx, wantUser.ID).Maybe().Return(nil, nil)
}

if tc.orgExist {
Expand Down