Skip to content
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