Skip to content

Add asymmetric JWT signing #16010

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

Merged
merged 16 commits into from
Jun 17, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions models/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ func GetActiveOAuth2Providers() ([]string, map[string]OAuth2Provider, error) {

// InitOAuth2 initialize the OAuth2 lib and register all active OAuth2 providers in the library
func InitOAuth2() error {
if err := oauth2.InitSigningKey(); err != nil {
return err
}
if err := oauth2.Init(x); err != nil {
return err
}
Expand Down
16 changes: 8 additions & 8 deletions models/oauth2_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"strings"
"time"

"code.gitea.io/gitea/modules/auth/oauth2"
"code.gitea.io/gitea/modules/secret"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"

Expand Down Expand Up @@ -540,10 +540,10 @@ type OAuth2Token struct {
// ParseOAuth2Token parses a singed jwt string
func ParseOAuth2Token(jwtToken string) (*OAuth2Token, error) {
parsedToken, err := jwt.ParseWithClaims(jwtToken, &OAuth2Token{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
if token.Method == nil || token.Method.Alg() != oauth2.DefaultSigningKey.SigningMethod().Alg() {
return nil, fmt.Errorf("unexpected signing algo: %v", token.Header["alg"])
}
return setting.OAuth2.JWTSecretBytes, nil
return oauth2.DefaultSigningKey.VerifyKey(), nil
})
if err != nil {
return nil, err
Expand All @@ -559,8 +559,8 @@ func ParseOAuth2Token(jwtToken string) (*OAuth2Token, error) {
// SignToken signs the token with the JWT secret
func (token *OAuth2Token) SignToken() (string, error) {
token.IssuedAt = time.Now().Unix()
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS512, token)
return jwtToken.SignedString(setting.OAuth2.JWTSecretBytes)
jwtToken := jwt.NewWithClaims(oauth2.DefaultSigningKey.SigningMethod(), token)
return jwtToken.SignedString(oauth2.DefaultSigningKey.SignKey())
}

// OIDCToken represents an OpenID Connect id_token
Expand All @@ -570,8 +570,8 @@ type OIDCToken struct {
}

// SignToken signs an id_token with the (symmetric) client secret key
func (token *OIDCToken) SignToken(clientSecret string) (string, error) {
func (token *OIDCToken) SignToken(signingKey oauth2.JWTSigningKey) (string, error) {
token.IssuedAt = time.Now().Unix()
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, token)
return jwtToken.SignedString([]byte(clientSecret))
jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token)
return jwtToken.SignedString(signingKey.SignKey())
}
190 changes: 190 additions & 0 deletions modules/auth/oauth2/jwtsigningkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package oauth2

import (
"crypto/ecdsa"
"crypto/rsa"
"encoding/base64"
"fmt"
"math/big"

"code.gitea.io/gitea/modules/setting"

"github.com/dgrijalva/jwt-go"
)

// ErrInvalidAlgorithmType represents an invalid algorithm error.
type ErrInvalidAlgorithmType struct {
Algorightm string
}

func (err ErrInvalidAlgorithmType) Error() string {
return fmt.Sprintf("JWT signing algorithm is not supported: %s", err.Algorightm)
}

// JWTSigningKey represents a algorithm/key pair to sign JWTs
type JWTSigningKey interface {
IsSymmetric() bool
SigningMethod() jwt.SigningMethod
SignKey() interface{}
VerifyKey() interface{}
ToJSON() map[string]string
}

type hmacSingingKey struct {
signingMethod jwt.SigningMethod
secret []byte
}

func (key hmacSingingKey) IsSymmetric() bool {
return true
}

func (key hmacSingingKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}

func (key hmacSingingKey) SignKey() interface{} {
return key.secret
}

func (key hmacSingingKey) VerifyKey() interface{} {
return key.secret
}

func (key hmacSingingKey) ToJSON() map[string]string {
return map[string]string{}
}

type rsaSingingKey struct {
signingMethod jwt.SigningMethod
key *rsa.PrivateKey
}

func (key rsaSingingKey) IsSymmetric() bool {
return false
}

func (key rsaSingingKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}

func (key rsaSingingKey) SignKey() interface{} {
return key.key
}

func (key rsaSingingKey) VerifyKey() interface{} {
return key.key.Public()
}

func (key rsaSingingKey) ToJSON() map[string]string {
pubKey := key.key.Public().(*rsa.PublicKey)

return map[string]string {
"kty": "RSA",
"alg": key.SigningMethod().Alg(),
"e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pubKey.E)).Bytes()),
"n": base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes()),
}
}

type ecdsaSingingKey struct {
signingMethod jwt.SigningMethod
key *ecdsa.PrivateKey
}

func (key ecdsaSingingKey) IsSymmetric() bool {
return false
}

func (key ecdsaSingingKey) SigningMethod() jwt.SigningMethod {
return key.signingMethod
}

func (key ecdsaSingingKey) SignKey() interface{} {
return key.key
}

func (key ecdsaSingingKey) VerifyKey() interface{} {
return key.key.Public()
}

func (key ecdsaSingingKey) ToJSON() map[string]string {
pubKey := key.key.Public().(*ecdsa.PublicKey)

return map[string]string {
"kty": "EC",
"alg": key.SigningMethod().Alg(),
"crv": pubKey.Params().Name,
"x": base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()),
"y": base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()),
}
}

// CreateJWTSingingKey creates a signing key from an algorithm / key pair.
func CreateJWTSingingKey(algorithm string, key interface{}) (JWTSigningKey, error) {
var signingMethod jwt.SigningMethod
switch algorithm {
case "HS256":
signingMethod = jwt.SigningMethodHS256
case "HS384":
signingMethod = jwt.SigningMethodHS384
case "HS512":
signingMethod = jwt.SigningMethodHS512

case "RS256":
signingMethod = jwt.SigningMethodRS256
case "RS384":
signingMethod = jwt.SigningMethodRS384
case "RS512":
signingMethod = jwt.SigningMethodRS512

case "ES256":
signingMethod = jwt.SigningMethodES256
case "ES384":
signingMethod = jwt.SigningMethodES384
case "ES512":
signingMethod = jwt.SigningMethodES512
default:
return nil, ErrInvalidAlgorithmType{algorithm}
}

switch signingMethod.(type) {
case *jwt.SigningMethodECDSA:
privateKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return ecdsaSingingKey{signingMethod, privateKey}, nil
case *jwt.SigningMethodRSA:
privateKey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return rsaSingingKey{signingMethod, privateKey}, nil
default:
secret, ok := key.([]byte)
if !ok {
return nil, jwt.ErrInvalidKeyType
}
return hmacSingingKey{signingMethod, secret}, nil
}
}

// DefaultSigningKey is the default signing key for JWTs.
var DefaultSigningKey JWTSigningKey

// InitSigningKey creates the default signing key from settings or creates a random key.
func InitSigningKey() error {
key, err := CreateJWTSingingKey("HS256", setting.OAuth2.JWTSecretBytes)
if err != nil {
return err
}

DefaultSigningKey = key

return nil
}
1 change: 1 addition & 0 deletions routers/routes/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ func RegisterRoutes(m *web.Route) {
} else {
m.Post("/login/oauth/access_token", bindIgnErr(forms.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
}
m.Get("/login/oauth/keys", ignSignInAndCsrf, user.OIDCKeys)

m.Group("/user/settings", func() {
m.Get("", userSetting.Profile)
Expand Down
53 changes: 43 additions & 10 deletions routers/user/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/auth/oauth2"
"code.gitea.io/gitea/modules/auth/sso"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
Expand All @@ -24,6 +25,7 @@ import (

"gitea.com/go-chi/binding"
"github.com/dgrijalva/jwt-go"
jsoniter "github.com/json-iterator/go"
)

const (
Expand Down Expand Up @@ -131,7 +133,7 @@ type AccessTokenResponse struct {
IDToken string `json:"id_token,omitempty"`
}

func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*AccessTokenResponse, *AccessTokenError) {
func newAccessTokenResponse(grant *models.OAuth2Grant, signingKey oauth2.JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
if setting.OAuth2.InvalidateRefreshTokens {
if err := grant.IncreaseCounter(); err != nil {
return nil, &AccessTokenError{
Expand Down Expand Up @@ -194,7 +196,7 @@ func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*Ac
},
Nonce: grant.Nonce,
}
signedIDToken, err = idToken.SignToken(clientSecret)
signedIDToken, err = idToken.SignToken(signingKey)
if err != nil {
return nil, &AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
Expand Down Expand Up @@ -451,12 +453,31 @@ func GrantApplicationOAuth(ctx *context.Context) {
func OIDCWellKnown(ctx *context.Context) {
t := ctx.Render.TemplateLookup("user/auth/oidc_wellknown")
ctx.Resp.Header().Set("Content-Type", "application/json")
ctx.Data["SigningKey"] = oauth2.DefaultSigningKey
if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
log.Error("%v", err)
ctx.Error(http.StatusInternalServerError)
}
}

// OIDCKeys generates the JSON Web Key Set
func OIDCKeys(ctx *context.Context) {
keyJSON := oauth2.DefaultSigningKey.ToJSON()
keyJSON["use"] = "sig"

jwkSet := map[string][]map[string]string {
"keys": []map[string]string {
keyJSON,
},
}

ctx.Resp.Header().Set("Content-Type", "application/json")
enc := jsoniter.NewEncoder(ctx.Resp)
if err := enc.Encode(jwkSet); err != nil {
log.Error("Failed to encode representation as json. Error: %v", err)
}
}

// AccessTokenOAuth manages all access token requests by the client
func AccessTokenOAuth(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.AccessTokenForm)
Expand Down Expand Up @@ -484,13 +505,25 @@ func AccessTokenOAuth(ctx *context.Context) {
form.ClientSecret = pair[1]
}
}

signingKey := oauth2.DefaultSigningKey
if signingKey.IsSymmetric() {
clientKey, err := oauth2.CreateJWTSingingKey(signingKey.SigningMethod().Alg(), []byte(form.ClientSecret))
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeInvalidRequest,
ErrorDescription: "Error creating signing key",
})
return
}
signingKey = clientKey
}

switch form.GrantType {
case "refresh_token":
handleRefreshToken(ctx, form)
return
handleRefreshToken(ctx, form, signingKey)
case "authorization_code":
handleAuthorizationCode(ctx, form)
return
handleAuthorizationCode(ctx, form, signingKey)
default:
handleAccessTokenError(ctx, AccessTokenError{
ErrorCode: AccessTokenErrorCodeUnsupportedGrantType,
Expand All @@ -499,7 +532,7 @@ func AccessTokenOAuth(ctx *context.Context) {
}
}

func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm) {
func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, signingKey oauth2.JWTSigningKey) {
token, err := models.ParseOAuth2Token(form.RefreshToken)
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
Expand Down Expand Up @@ -527,15 +560,15 @@ func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm) {
log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID)
return
}
accessToken, tokenErr := newAccessTokenResponse(grant, form.ClientSecret)
accessToken, tokenErr := newAccessTokenResponse(grant, signingKey)
if tokenErr != nil {
handleAccessTokenError(ctx, *tokenErr)
return
}
ctx.JSON(http.StatusOK, accessToken)
}

func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm) {
func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, signingKey oauth2.JWTSigningKey) {
app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
if err != nil {
handleAccessTokenError(ctx, AccessTokenError{
Expand Down Expand Up @@ -589,7 +622,7 @@ func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm) {
ErrorDescription: "cannot proceed your request",
})
}
resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, form.ClientSecret)
resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, signingKey)
if tokenErr != nil {
handleAccessTokenError(ctx, *tokenErr)
return
Expand Down
Loading