Skip to content

Commit

Permalink
Update JWT lib to v5 (#4157)
Browse files Browse the repository at this point in the history
  • Loading branch information
nono authored Oct 17, 2023
2 parents a062d91 + 9d2a756 commit 5ba7ed9
Show file tree
Hide file tree
Showing 26 changed files with 114 additions and 119 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/dustin/go-humanize v1.0.1
github.com/gavv/httpexpect/v2 v2.16.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
github.com/goodsign/monday v1.0.1
github.com/google/go-querystring v1.1.0
Expand Down Expand Up @@ -67,6 +67,7 @@ require (
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzq
github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=
Expand Down
20 changes: 11 additions & 9 deletions model/bitwarden/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package bitwarden
import (
"strconv"
"strings"
"time"

"github.com/cozy/cozy-stack/model/bitwarden/settings"
"github.com/cozy/cozy-stack/model/instance"
"github.com/cozy/cozy-stack/model/oauth"
"github.com/cozy/cozy-stack/model/permission"
"github.com/cozy/cozy-stack/pkg/consts"
"github.com/cozy/cozy-stack/pkg/crypto"
"github.com/golang-jwt/jwt/v5"
)

// BitwardenScope is the OAuth scope, and it is hard-coded with the doctypes
Expand Down Expand Up @@ -93,7 +95,7 @@ func ParseBitwardenDeviceType(deviceType string) (string, string) {
// apps. It is an access token, with some additional custom fields.
// See https://github.com/bitwarden/jslib/blob/master/common/src/services/token.service.ts
func CreateAccessJWT(i *instance.Instance, c *oauth.Client) (string, error) {
now := crypto.Timestamp()
now := time.Now()
name, err := i.SettingsPublicName()
if err != nil || name == "" {
name = "Anonymous"
Expand All @@ -104,12 +106,12 @@ func CreateAccessJWT(i *instance.Instance, c *oauth.Client) (string, error) {
}
token, err := crypto.NewJWT(i.OAuthSecret, permission.BitwardenClaims{
Claims: permission.Claims{
StandardClaims: crypto.StandardClaims{
Audience: consts.AccessTokenAudience,
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{consts.AccessTokenAudience},
Issuer: i.Domain,
NotBefore: now - 60,
IssuedAt: now,
ExpiresAt: now + int64(consts.AccessTokenValidityDuration.Seconds()),
NotBefore: jwt.NewNumericDate(now.Add(-60 * time.Second)),
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(consts.AccessTokenValidityDuration)),
Subject: i.ID(),
},
SStamp: stamp,
Expand All @@ -136,10 +138,10 @@ func CreateRefreshJWT(i *instance.Instance, c *oauth.Client) (string, error) {
stamp = settings.SecurityStamp
}
token, err := crypto.NewJWT(i.OAuthSecret, permission.Claims{
StandardClaims: crypto.StandardClaims{
Audience: consts.RefreshTokenAudience,
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{consts.RefreshTokenAudience},
Issuer: i.Domain,
IssuedAt: crypto.Timestamp(),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: c.CouchID,
},
SStamp: stamp,
Expand Down
7 changes: 4 additions & 3 deletions model/instance/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/cozy/cozy-stack/pkg/logger"
"github.com/cozy/cozy-stack/pkg/prefixer"
"github.com/cozy/cozy-stack/pkg/realtime"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/afero"
)

Expand Down Expand Up @@ -667,10 +668,10 @@ func (i *Instance) MakeJWT(audience, subject, scope, sessionID string, issuedAt
return "", err
}
return crypto.NewJWT(secret, permission.Claims{
StandardClaims: crypto.StandardClaims{
Audience: audience,
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{audience},
Issuer: i.Domain,
IssuedAt: issuedAt.Unix(),
IssuedAt: jwt.NewNumericDate(issuedAt),
Subject: subject,
},
Scope: scope,
Expand Down
4 changes: 2 additions & 2 deletions model/instance/instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/cozy/cozy-stack/model/instance"
"github.com/cozy/cozy-stack/pkg/config/config"
"github.com/cozy/cozy-stack/pkg/crypto"
jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -47,7 +47,7 @@ func TestInstance(t *testing.T) {

claims, ok := token.Claims.(jwt.MapClaims)
assert.True(t, ok, "Claims can be parsed as standard claims")
assert.Equal(t, "app", claims["aud"])
assert.Equal(t, []interface{}{"app"}, claims["aud"])
assert.Equal(t, "test-ctx-token.example.com", claims["iss"])
assert.Equal(t, "my-app", claims["sub"])
})
Expand Down
2 changes: 1 addition & 1 deletion model/move/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/cozy/cozy-stack/pkg/couchdb"
"github.com/cozy/cozy-stack/pkg/crypto"
"github.com/cozy/cozy-stack/pkg/safehttp"
jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"
multierror "github.com/hashicorp/go-multierror"
"github.com/labstack/echo/v4"
)
Expand Down
2 changes: 1 addition & 1 deletion model/oauth/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"github.com/cozy/cozy-stack/model/instance"
"github.com/cozy/cozy-stack/pkg/config/config"
"github.com/cozy/cozy-stack/pkg/logger"
jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"
)

// checkAndroidAttestation will check an attestation made by the SafetyNet API.
Expand Down
18 changes: 9 additions & 9 deletions model/oauth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"github.com/cozy/cozy-stack/pkg/metadata"
"github.com/cozy/cozy-stack/pkg/registry"

jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"
)

const (
Expand Down Expand Up @@ -500,10 +500,10 @@ func (c *Client) Create(i *instance.Instance, opts ...CreateOptions) *ClientRegi
}

var err error
c.RegistrationToken, err = crypto.NewJWT(i.OAuthSecret, crypto.StandardClaims{
Audience: consts.RegistrationTokenAudience,
c.RegistrationToken, err = crypto.NewJWT(i.OAuthSecret, jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{consts.RegistrationTokenAudience},
Issuer: i.Domain,
IssuedAt: time.Now().Unix(),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: c.CouchID,
})
if err != nil {
Expand Down Expand Up @@ -730,10 +730,10 @@ func (c *Client) AcceptRedirectURI(u string) bool {
// CreateJWT returns a new JSON Web Token for the given instance and audience
func (c *Client) CreateJWT(i *instance.Instance, audience, scope string) (string, error) {
token, err := crypto.NewJWT(i.OAuthSecret, permission.Claims{
StandardClaims: crypto.StandardClaims{
Audience: audience,
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{audience},
Issuer: i.Domain,
IssuedAt: crypto.Timestamp(),
IssuedAt: jwt.NewNumericDate(time.Now()),
Subject: c.CouchID,
},
Scope: scope,
Expand Down Expand Up @@ -764,9 +764,9 @@ func validToken(i *instance.Instance, audience, token string) (permission.Claims
return claims, false
}
// Note: the refresh and registration tokens don't expire, no need to check its issue date
if claims.Audience != audience {
if claims.AudienceString() != audience {
i.Logger().WithNamespace("oauth").
Errorf("Unexpected audience for %s token: %s", audience, claims.Audience)
Errorf("Unexpected audience for %s token: %v", audience, claims.Audience)
return claims, false
}
if claims.Issuer != i.Domain {
Expand Down
6 changes: 3 additions & 3 deletions model/oauth/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/cozy/cozy-stack/pkg/couchdb/mango"
"github.com/cozy/cozy-stack/pkg/metadata"
"github.com/cozy/cozy-stack/tests/testutils"
jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -51,7 +51,7 @@ func TestClient(t *testing.T) {

claims, ok := token.Claims.(jwt.MapClaims)
assert.True(t, ok, "Claims can be parsed as standard claims")
assert.Equal(t, "test", claims["aud"])
assert.Equal(t, []interface{}{"test"}, claims["aud"])
assert.Equal(t, testInstance.Domain, claims["iss"])
assert.Equal(t, "my-client-id", claims["sub"])
assert.Equal(t, "foo:read", claims["scope"])
Expand All @@ -63,7 +63,7 @@ func TestClient(t *testing.T) {

claims, ok := c.ValidToken(testInstance, consts.RefreshTokenAudience, tokenString)
assert.True(t, ok, "The token must be valid")
assert.Equal(t, "refresh", claims.Audience)
assert.Equal(t, jwt.ClaimStrings{"refresh"}, claims.Audience)
assert.Equal(t, testInstance.Domain, claims.Issuer)
assert.Equal(t, "my-client-id", claims.Subject)
assert.Equal(t, "foo:read", claims.Scope)
Expand Down
10 changes: 7 additions & 3 deletions model/office/callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/cozy/cozy-stack/pkg/crypto"
"github.com/cozy/cozy-stack/pkg/metadata"

jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"
)

// Status list is described on https://api.onlyoffice.com/editors/callback#status
Expand Down Expand Up @@ -58,8 +58,12 @@ type callbackClaims struct {
} `json:"payload"`
}

// Valid is part of the jwt.Claims interface
func (c *callbackClaims) Valid() error { return nil }
func (c *callbackClaims) GetExpirationTime() (*jwt.NumericDate, error) { return nil, nil }
func (c *callbackClaims) GetIssuedAt() (*jwt.NumericDate, error) { return nil, nil }
func (c *callbackClaims) GetNotBefore() (*jwt.NumericDate, error) { return nil, nil }
func (c *callbackClaims) GetIssuer() (string, error) { return "", nil }
func (c *callbackClaims) GetSubject() (string, error) { return "", nil }
func (c *callbackClaims) GetAudience() (jwt.ClaimStrings, error) { return nil, nil }

// Callback will manage the callback from the document server.
func Callback(inst *instance.Instance, params CallbackParameters) error {
Expand Down
10 changes: 7 additions & 3 deletions model/office/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/cozy/cozy-stack/pkg/consts"
"github.com/cozy/cozy-stack/pkg/couchdb"
"github.com/cozy/cozy-stack/pkg/jsonapi"
jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"
)

type apiOfficeURL struct {
Expand Down Expand Up @@ -93,8 +93,12 @@ func (o *apiOfficeURL) sign(cfg *config.Office) (string, error) {
return token.SignedString([]byte(cfg.InboxSecret))
}

// Valid is a method of the jwt.Claims interface
func (o *onlyOffice) Valid() error { return nil }
func (o *onlyOffice) GetExpirationTime() (*jwt.NumericDate, error) { return nil, nil }
func (o *onlyOffice) GetIssuedAt() (*jwt.NumericDate, error) { return nil, nil }
func (o *onlyOffice) GetNotBefore() (*jwt.NumericDate, error) { return nil, nil }
func (o *onlyOffice) GetIssuer() (string, error) { return "", nil }
func (o *onlyOffice) GetSubject() (string, error) { return "", nil }
func (o *onlyOffice) GetAudience() (jwt.ClaimStrings, error) { return nil, nil }

// Opener can be used to find the parameters for opening an office document.
type Opener struct {
Expand Down
20 changes: 16 additions & 4 deletions model/permission/claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import (
"time"

"github.com/cozy/cozy-stack/pkg/consts"
"github.com/cozy/cozy-stack/pkg/crypto"
"github.com/golang-jwt/jwt/v5"
)

// Claims is used for JWT used in OAuth2 flow and applications token
type Claims struct {
crypto.StandardClaims
jwt.RegisteredClaims
Scope string `json:"scope,omitempty"`
SessionID string `json:"session_id,omitempty"`
SStamp string `json:"stamp,omitempty"`
Expand All @@ -18,13 +18,25 @@ type Claims struct {
// IssuedAtUTC returns a time.Time struct of the IssuedAt field in UTC
// location.
func (claims *Claims) IssuedAtUTC() time.Time {
return time.Unix(claims.IssuedAt, 0).UTC()
return claims.IssuedAt.Time.UTC()
}

// AudienceString returns the audience as a string.
func (claims *Claims) AudienceString() string {
if len(claims.Audience) == 0 {
return ""
}
return claims.Audience[0]
}

// Expired returns true if a Claim is expired
func (claims *Claims) Expired() bool {
if len(claims.Audience) != 1 {
return true
}

var validityDuration time.Duration
switch claims.Audience {
switch claims.Audience[0] {
case consts.AppAudience:
if claims.SessionID == "" {
// an app token with no session association is used for services which
Expand Down
2 changes: 1 addition & 1 deletion model/session/delegated.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"errors"
"time"

jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"

"github.com/cozy/cozy-stack/model/instance"
"github.com/cozy/cozy-stack/pkg/config/config"
Expand Down
2 changes: 1 addition & 1 deletion model/session/delegated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/cozy/cozy-stack/model/instance"
"github.com/cozy/cozy-stack/pkg/config/config"

jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
)

Expand Down
2 changes: 1 addition & 1 deletion model/sharing/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"github.com/cozy/cozy-stack/pkg/couchdb/mango"
"github.com/cozy/cozy-stack/pkg/jsonapi"
"github.com/cozy/cozy-stack/pkg/safehttp"
jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)

Expand Down
39 changes: 1 addition & 38 deletions pkg/crypto/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,10 @@ package crypto
import (
"errors"
"fmt"
"time"

jwt "github.com/golang-jwt/jwt/v4"
jwt "github.com/golang-jwt/jwt/v5"
)

// StandardClaims are a structured version of the JWT Claims Set, as referenced at
// https://datatracker.ietf.org/doc/html/rfc7519#section-4. They do not follow the
// specification exactly, since they were based on an earlier draft of the
// specification and not updated. The main difference is that they only
// support integer-based date fields and singular audiences.
type StandardClaims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}

// Valid validates time based claims "exp, iat, nbf". There is no accounting
// for clock skew. As well, if any of the above claims are not in the token, it
// will still be considered a valid claim.
func (claims StandardClaims) Valid() error {
now := time.Now().Unix()

if claims.IssuedAt > now {
return fmt.Errorf("token used before issued")
}

// The claims below are optional, by default, so if they are set to the
// default value in Go, let's not fail the verification for them.
if claims.ExpiresAt > 0 && claims.ExpiresAt < now {
return fmt.Errorf("token is expired by %v", now-claims.ExpiresAt)
}
if claims.NotBefore > 0 && claims.NotBefore > now {
return fmt.Errorf("token is not valid yet")
}

return nil
}

// SigningMethod is the algorithm choosed for signing JWT.
// Currently, it is HMAC-SHA-512
var SigningMethod = jwt.SigningMethodHS512
Expand Down
Loading

0 comments on commit 5ba7ed9

Please sign in to comment.