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

Add asymmetric JWT signing #16010

Merged
merged 16 commits into from
Jun 17, 2021
Merged
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
Next Next commit
Added asymmetric token signing.
KN4CK3R committed May 28, 2021
commit c99a53cf16eb5724bb09d304e37a1e59a24b3908
3 changes: 3 additions & 0 deletions models/oauth2.go
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 8 additions & 8 deletions models/oauth2_application.go
Original file line number Diff line number Diff line change
@@ -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"

@@ -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
@@ -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
@@ -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
@@ -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)
53 changes: 43 additions & 10 deletions routers/user/oauth.go
Original file line number Diff line number Diff line change
@@ -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"
@@ -24,6 +25,7 @@ import (

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

const (
@@ -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{
@@ -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,
@@ -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)
@@ -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,
@@ -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{
@@ -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{
@@ -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
7 changes: 7 additions & 0 deletions templates/user/auth/oidc_wellknown.tmpl
Original file line number Diff line number Diff line change
@@ -2,9 +2,16 @@
"issuer": "{{AppUrl | JSEscape | Safe}}",
"authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize",
"token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token",
"jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys",
"userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo",
"response_types_supported": [
"code",
"id_token"
],
"id_token_signing_alg_values_supported": [
"{{.SigningKey.SigningMethod.Alg | JSEscape | Safe}}"
],
"subject_types_supported": [
"public"
]
}