diff --git a/server/src/api/router.go b/server/src/api/router.go
index 9d66fe1e21..83d72063a5 100644
--- a/server/src/api/router.go
+++ b/server/src/api/router.go
@@ -1,6 +1,8 @@
package api
import (
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/chi/v5/middleware"
"github.com/markbates/goth/gothic"
"net/http"
"os"
@@ -8,7 +10,6 @@ import (
"github.com/go-chi/cors"
"github.com/go-chi/httprate"
- "github.com/go-chi/jwtauth/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
gorillaSessions "github.com/gorilla/sessions"
@@ -18,9 +19,6 @@ import (
"scrumlr.io/server/logger"
"scrumlr.io/server/realtime"
"scrumlr.io/server/services"
-
- "github.com/go-chi/chi/v5"
- "github.com/go-chi/chi/v5/middleware"
)
type Server struct {
@@ -171,7 +169,7 @@ func (s *Server) publicRoutes(r chi.Router) chi.Router {
func (s *Server) protectedRoutes(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(s.auth.Verifier())
- r.Use(jwtauth.Authenticator)
+ r.Use(s.auth.Authenticator())
r.Use(auth.AuthContext)
r.With(s.BoardTemplateRateLimiter).Post("/templates", s.createBoardTemplate)
diff --git a/server/src/auth/auth.go b/server/src/auth/auth.go
index ba3757a17b..3f43158058 100644
--- a/server/src/auth/auth.go
+++ b/server/src/auth/auth.go
@@ -1,289 +1,294 @@
package auth
import (
- "crypto/ecdsa"
- "crypto/rand"
- "encoding/base64"
- "errors"
- "fmt"
- "io"
- "math"
- "net/http"
- "strings"
-
- "github.com/go-chi/chi/v5"
- "github.com/go-chi/jwtauth/v5"
- "github.com/google/uuid"
- "github.com/lestrrat-go/jwx/v2/jwt"
- "github.com/markbates/goth"
- "github.com/markbates/goth/gothic"
- "github.com/markbates/goth/providers/apple"
- "github.com/markbates/goth/providers/azureadv2"
- "github.com/markbates/goth/providers/github"
- "github.com/markbates/goth/providers/google"
- "github.com/markbates/goth/providers/microsoftonline"
- oidc "github.com/markbates/goth/providers/openidConnect"
- "golang.org/x/crypto/ssh"
- "scrumlr.io/server/auth/devkeys"
- "scrumlr.io/server/common"
- "scrumlr.io/server/database"
- "scrumlr.io/server/database/types"
- "scrumlr.io/server/logger"
+ "crypto/ecdsa"
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "net/http"
+ "strings"
+
+ "github.com/go-chi/chi/v5"
+ "github.com/go-chi/jwtauth/v5"
+ "github.com/google/uuid"
+ "github.com/lestrrat-go/jwx/v2/jwt"
+ "github.com/markbates/goth"
+ "github.com/markbates/goth/gothic"
+ "github.com/markbates/goth/providers/apple"
+ "github.com/markbates/goth/providers/azureadv2"
+ "github.com/markbates/goth/providers/github"
+ "github.com/markbates/goth/providers/google"
+ "github.com/markbates/goth/providers/microsoftonline"
+ oidc "github.com/markbates/goth/providers/openidConnect"
+ "golang.org/x/crypto/ssh"
+ "scrumlr.io/server/auth/devkeys"
+ "scrumlr.io/server/common"
+ "scrumlr.io/server/database"
+ "scrumlr.io/server/database/types"
+ "scrumlr.io/server/logger"
)
type Auth interface {
- Sign(map[string]interface{}) (string, error)
- Verifier() func(http.Handler) http.Handler
- Exists(accountType types.AccountType) bool
- ExtractUserInformation(types.AccountType, *goth.User) (*UserInformation, error)
+ Sign(map[string]interface{}) (string, error)
+ Verifier() func(http.Handler) http.Handler
+ Authenticator() func(http.Handler) http.Handler
+ Exists(accountType types.AccountType) bool
+ ExtractUserInformation(types.AccountType, *goth.User) (*UserInformation, error)
}
type AuthProviderConfiguration struct {
- TenantId string
- ClientId string
- ClientSecret string
- RedirectUri string
- DiscoveryUri string
- UserIdentScope string
- UserNameScope string
+ TenantId string
+ ClientId string
+ ClientSecret string
+ RedirectUri string
+ DiscoveryUri string
+ UserIdentScope string
+ UserNameScope string
}
type AuthConfiguration struct {
- providers map[string]AuthProviderConfiguration
- unsafePrivateKey string
- privateKey string
- unsafeAuth *jwtauth.JWTAuth
- auth *jwtauth.JWTAuth
- database *database.Database
+ providers map[string]AuthProviderConfiguration
+ unsafePrivateKey string
+ privateKey string
+ unsafeAuth *jwtauth.JWTAuth
+ auth *jwtauth.JWTAuth
+ database *database.Database
}
type UserInformation struct {
- Provider types.AccountType
- Ident, Name, AvatarURL string
+ Provider types.AccountType
+ Ident, Name, AvatarURL string
}
func NewAuthConfiguration(providers map[string]AuthProviderConfiguration, unsafePrivateKey, privateKey string, database *database.Database) (Auth, error) {
- a := new(AuthConfiguration)
- a.providers = providers
- a.unsafePrivateKey = unsafePrivateKey
- a.database = database
- a.privateKey = privateKey
- if err := a.initializeProviders(); err != nil {
- return nil, err
- }
- if err := a.initializeJWTAuth(); err != nil {
- return nil, err
- }
-
- return a, nil
+ a := new(AuthConfiguration)
+ a.providers = providers
+ a.unsafePrivateKey = unsafePrivateKey
+ a.database = database
+ a.privateKey = privateKey
+ if err := a.initializeProviders(); err != nil {
+ return nil, err
+ }
+ if err := a.initializeJWTAuth(); err != nil {
+ return nil, err
+ }
+
+ return a, nil
}
func (a *AuthConfiguration) initializeProviders() error {
- providers := []goth.Provider{}
- if provider, ok := a.providers[(string)(types.AccountTypeGoogle)]; ok {
- p := google.New(
- provider.ClientId,
- provider.ClientSecret,
- provider.RedirectUri,
- "openid",
- "profile",
- )
- p.SetName(strings.ToLower((string)(types.AccountTypeGoogle)))
- providers = append(providers, p)
- }
- if provider, ok := a.providers[(string)(types.AccountTypeGitHub)]; ok {
- p := github.New(
- provider.ClientId,
- provider.ClientSecret,
- provider.RedirectUri,
- "user",
- )
- p.SetName(strings.ToLower((string)(types.AccountTypeGitHub)))
- providers = append(providers, p)
- }
- if provider, ok := a.providers[(string)(types.AccountTypeMicrosoft)]; ok {
- p := microsoftonline.New(
- provider.ClientId,
- provider.ClientSecret,
- provider.RedirectUri,
- "User.Read",
- )
- p.SetName(strings.ToLower((string)(types.AccountTypeMicrosoft)))
- providers = append(providers, p)
- }
- if provider, ok := a.providers[(string)(types.AccountTypeAzureAd)]; ok {
- p := azureadv2.New(
- provider.ClientId,
- provider.ClientSecret,
- provider.RedirectUri,
- azureadv2.ProviderOptions{
- Tenant: azureadv2.TenantType(provider.TenantId),
- Scopes: []azureadv2.ScopeType{"User.Read"},
- },
- )
- p.SetName(strings.ToLower((string)(types.AccountTypeAzureAd)))
- providers = append(providers, p)
- }
- if provider, ok := a.providers[(string)(types.AccountTypeApple)]; ok {
- providers = append(providers, apple.New(
- provider.ClientId,
- provider.ClientSecret,
- provider.RedirectUri,
- nil,
- apple.ScopeName,
- apple.ScopeEmail,
- ))
- }
- if provider, ok := a.providers[(string)(types.AccountTypeOIDC)]; ok {
- p, err := oidc.New(
- provider.ClientId,
- provider.ClientSecret,
- provider.RedirectUri,
- provider.DiscoveryUri,
- provider.UserIdentScope,
- provider.UserNameScope,
- )
- if err != nil {
- logger.Get().Errorw("OIDC provider setup failed", "error", err)
- }
-
- p.SetName(strings.ToLower((string)(types.AccountTypeOIDC)))
- providers = append(providers, p)
- }
- goth.UseProviders(providers...)
- gothic.GetProviderName = func(r *http.Request) (string, error) {
- return chi.URLParam(r, "provider"), nil
- }
- gothic.SetState = func(r *http.Request) string {
- nonceBytes := make([]byte, 64)
- _, err := io.ReadFull(rand.Reader, nonceBytes)
- if err != nil {
- panic("gothic: source of randomness unavailable: " + err.Error())
- }
- nonce := base64.URLEncoding.EncodeToString(nonceBytes)
-
- state := r.URL.Query().Get("state")
- if len(state) > 0 {
- return fmt.Sprintf("%s__%s", nonce, state)
- }
-
- return nonce
- }
-
- return nil
+ providers := []goth.Provider{}
+ if provider, ok := a.providers[(string)(types.AccountTypeGoogle)]; ok {
+ p := google.New(
+ provider.ClientId,
+ provider.ClientSecret,
+ provider.RedirectUri,
+ "openid",
+ "profile",
+ )
+ p.SetName(strings.ToLower((string)(types.AccountTypeGoogle)))
+ providers = append(providers, p)
+ }
+ if provider, ok := a.providers[(string)(types.AccountTypeGitHub)]; ok {
+ p := github.New(
+ provider.ClientId,
+ provider.ClientSecret,
+ provider.RedirectUri,
+ "user",
+ )
+ p.SetName(strings.ToLower((string)(types.AccountTypeGitHub)))
+ providers = append(providers, p)
+ }
+ if provider, ok := a.providers[(string)(types.AccountTypeMicrosoft)]; ok {
+ p := microsoftonline.New(
+ provider.ClientId,
+ provider.ClientSecret,
+ provider.RedirectUri,
+ "User.Read",
+ )
+ p.SetName(strings.ToLower((string)(types.AccountTypeMicrosoft)))
+ providers = append(providers, p)
+ }
+ if provider, ok := a.providers[(string)(types.AccountTypeAzureAd)]; ok {
+ p := azureadv2.New(
+ provider.ClientId,
+ provider.ClientSecret,
+ provider.RedirectUri,
+ azureadv2.ProviderOptions{
+ Tenant: azureadv2.TenantType(provider.TenantId),
+ Scopes: []azureadv2.ScopeType{"User.Read"},
+ },
+ )
+ p.SetName(strings.ToLower((string)(types.AccountTypeAzureAd)))
+ providers = append(providers, p)
+ }
+ if provider, ok := a.providers[(string)(types.AccountTypeApple)]; ok {
+ providers = append(providers, apple.New(
+ provider.ClientId,
+ provider.ClientSecret,
+ provider.RedirectUri,
+ nil,
+ apple.ScopeName,
+ apple.ScopeEmail,
+ ))
+ }
+ if provider, ok := a.providers[(string)(types.AccountTypeOIDC)]; ok {
+ p, err := oidc.New(
+ provider.ClientId,
+ provider.ClientSecret,
+ provider.RedirectUri,
+ provider.DiscoveryUri,
+ provider.UserIdentScope,
+ provider.UserNameScope,
+ )
+ if err != nil {
+ logger.Get().Errorw("OIDC provider setup failed", "error", err)
+ }
+
+ p.SetName(strings.ToLower((string)(types.AccountTypeOIDC)))
+ providers = append(providers, p)
+ }
+ goth.UseProviders(providers...)
+ gothic.GetProviderName = func(r *http.Request) (string, error) {
+ return chi.URLParam(r, "provider"), nil
+ }
+ gothic.SetState = func(r *http.Request) string {
+ nonceBytes := make([]byte, 64)
+ _, err := io.ReadFull(rand.Reader, nonceBytes)
+ if err != nil {
+ panic("gothic: source of randomness unavailable: " + err.Error())
+ }
+ nonce := base64.URLEncoding.EncodeToString(nonceBytes)
+
+ state := r.URL.Query().Get("state")
+ if len(state) > 0 {
+ return fmt.Sprintf("%s__%s", nonce, state)
+ }
+
+ return nonce
+ }
+
+ return nil
}
func (a *AuthConfiguration) Sign(claims map[string]interface{}) (string, error) {
- _, token, err := a.auth.Encode(claims)
- return token, err
+ _, token, err := a.auth.Encode(claims)
+ return token, err
}
func (a *AuthConfiguration) Verifier() func(http.Handler) http.Handler {
- if a.unsafeAuth != nil {
- return func(next http.Handler) http.Handler {
- hfn := func(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
-
- var token jwt.Token
- var err error
-
- if token, err = jwtauth.VerifyRequest(a.unsafeAuth, r, jwtauth.TokenFromCookie); err == nil {
- // check if user tries to authenticate by a prior authentication key
- // attempt to migrate JWT to new key
- userID := token.PrivateClaims()["id"].(string)
- var user uuid.UUID
- user, err = uuid.Parse(userID)
-
- if err == nil {
- var ok bool
- if ok, err = a.database.IsUserAvailableForKeyMigration(user); ok {
- // prepare new JWT
- tokenString, _ := a.Sign(map[string]interface{}{"id": user})
- cookie := http.Cookie{Name: "jwt", Value: tokenString, Path: "/", HttpOnly: true, MaxAge: math.MaxInt32}
- common.SealCookie(r, &cookie)
- http.SetCookie(w, &cookie)
-
- // update rotation flag in database for user, ignore errors
- _, _ = a.database.SetKeyMigration(user)
- } else {
- err = errors.New("not permitted to access key rotation")
- }
- }
- } else {
- // attempt to verify request by new key
- token, err = jwtauth.VerifyRequest(a.auth, r, jwtauth.TokenFromCookie)
- }
-
- ctx = jwtauth.NewContext(ctx, token, err)
- next.ServeHTTP(w, r.WithContext(ctx))
- }
- return http.HandlerFunc(hfn)
- }
- }
- return jwtauth.Verifier(a.auth)
+ if a.unsafeAuth != nil {
+ return func(next http.Handler) http.Handler {
+ hfn := func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ var token jwt.Token
+ var err error
+
+ if token, err = jwtauth.VerifyRequest(a.unsafeAuth, r, jwtauth.TokenFromCookie); err == nil {
+ // check if user tries to authenticate by a prior authentication key
+ // attempt to migrate JWT to new key
+ userID := token.PrivateClaims()["id"].(string)
+ var user uuid.UUID
+ user, err = uuid.Parse(userID)
+
+ if err == nil {
+ var ok bool
+ if ok, err = a.database.IsUserAvailableForKeyMigration(user); ok {
+ // prepare new JWT
+ tokenString, _ := a.Sign(map[string]interface{}{"id": user})
+ cookie := http.Cookie{Name: "jwt", Value: tokenString, Path: "/", HttpOnly: true, MaxAge: math.MaxInt32}
+ common.SealCookie(r, &cookie)
+ http.SetCookie(w, &cookie)
+
+ // update rotation flag in database for user, ignore errors
+ _, _ = a.database.SetKeyMigration(user)
+ } else {
+ err = errors.New("not permitted to access key rotation")
+ }
+ }
+ } else {
+ // attempt to verify request by new key
+ token, err = jwtauth.VerifyRequest(a.auth, r, jwtauth.TokenFromCookie)
+ }
+
+ ctx = jwtauth.NewContext(ctx, token, err)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ }
+ return http.HandlerFunc(hfn)
+ }
+ }
+ return jwtauth.Verifier(a.auth)
+}
+
+func (a *AuthConfiguration) Authenticator() func(http.Handler) http.Handler {
+ return jwtauth.Authenticator(a.auth)
}
func (a *AuthConfiguration) Exists(accountType types.AccountType) bool {
- if _, ok := a.providers[string(accountType)]; ok {
- return true
- }
- return false
+ if _, ok := a.providers[string(accountType)]; ok {
+ return true
+ }
+ return false
}
func (a *AuthConfiguration) ExtractUserInformation(accountType types.AccountType, user *goth.User) (*UserInformation, error) {
- ident := user.UserID
- name := user.NickName
- avatar := user.AvatarURL
-
- if ident == "" {
- return nil, fmt.Errorf("unable to extract identifier information for user")
- }
-
- if name == "" {
- name = user.Name
- }
-
- if name == "" {
- return nil, fmt.Errorf("unable to extract name information for user %q", ident)
- }
-
- result := &UserInformation{
- Provider: accountType,
- Ident: ident,
- Name: name,
- AvatarURL: avatar,
- }
-
- return result, nil
+ ident := user.UserID
+ name := user.NickName
+ avatar := user.AvatarURL
+
+ if ident == "" {
+ return nil, fmt.Errorf("unable to extract identifier information for user")
+ }
+
+ if name == "" {
+ name = user.Name
+ }
+
+ if name == "" {
+ return nil, fmt.Errorf("unable to extract name information for user %q", ident)
+ }
+
+ result := &UserInformation{
+ Provider: accountType,
+ Ident: ident,
+ Name: name,
+ AvatarURL: avatar,
+ }
+
+ return result, nil
}
func (a *AuthConfiguration) initializeJWTAuth() error {
- if a.privateKey == "" {
- logger.Get().Warnw("invalid keypair config, falling back to dev keys!")
- a.privateKey = devkeys.PrivateKey
- }
-
- if a.unsafePrivateKey != "" {
- unsafeKey, err := ssh.ParseRawPrivateKey([]byte(a.unsafePrivateKey))
- if err != nil {
- return fmt.Errorf("unable parse unsafe auth keys: %w", err)
- }
- unsafePrivateKey, ok := unsafeKey.(*ecdsa.PrivateKey)
- if !ok {
- return errors.New("the provided unsafe keys are no ecdsa keys")
- }
- a.unsafeAuth = jwtauth.New("ES512", unsafePrivateKey, unsafePrivateKey.PublicKey)
- }
-
- key, err := ssh.ParseRawPrivateKey([]byte(a.privateKey))
- if err != nil {
- return fmt.Errorf("unable to parse auth keys: %w", err)
- }
- privateKey, ok := key.(*ecdsa.PrivateKey)
- if !ok {
- return errors.New("the provided keys are no ecdsa keys")
- }
-
- a.auth = jwtauth.New("ES512", privateKey, privateKey.PublicKey)
- return nil
+ if a.privateKey == "" {
+ logger.Get().Warnw("invalid keypair config, falling back to dev keys!")
+ a.privateKey = devkeys.PrivateKey
+ }
+
+ if a.unsafePrivateKey != "" {
+ unsafeKey, err := ssh.ParseRawPrivateKey([]byte(a.unsafePrivateKey))
+ if err != nil {
+ return fmt.Errorf("unable parse unsafe auth keys: %w", err)
+ }
+ unsafePrivateKey, ok := unsafeKey.(*ecdsa.PrivateKey)
+ if !ok {
+ return errors.New("the provided unsafe keys are no ecdsa keys")
+ }
+ a.unsafeAuth = jwtauth.New("ES512", unsafePrivateKey, unsafePrivateKey.PublicKey)
+ }
+
+ key, err := ssh.ParseRawPrivateKey([]byte(a.privateKey))
+ if err != nil {
+ return fmt.Errorf("unable to parse auth keys: %w", err)
+ }
+ privateKey, ok := key.(*ecdsa.PrivateKey)
+ if !ok {
+ return errors.New("the provided keys are no ecdsa keys")
+ }
+
+ a.auth = jwtauth.New("ES512", privateKey, privateKey.PublicKey)
+ return nil
}
diff --git a/server/src/go.mod b/server/src/go.mod
index 362b4ec90b..fa889c7759 100644
--- a/server/src/go.mod
+++ b/server/src/go.mod
@@ -11,7 +11,7 @@ require (
github.com/go-chi/chi/v5 v5.2.0
github.com/go-chi/cors v1.2.1
github.com/go-chi/httprate v0.14.1
- github.com/go-chi/jwtauth/v5 v5.1.1
+ github.com/go-chi/jwtauth/v5 v5.3.2
github.com/go-chi/render v1.0.3
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-migrate/migrate/v4 v4.18.1
diff --git a/server/src/go.sum b/server/src/go.sum
index 67bd7e9168..01c5009ee4 100644
--- a/server/src/go.sum
+++ b/server/src/go.sum
@@ -68,8 +68,8 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/httprate v0.14.1 h1:EKZHYEZ58Cg6hWcYzoZILsv7ppb46Wt4uQ738IRtpZs=
github.com/go-chi/httprate v0.14.1/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0=
-github.com/go-chi/jwtauth/v5 v5.1.1 h1:Pjixqu5YkjE9sCLpzE01L0Q4sQzJIPdo7uz9r8ftp/c=
-github.com/go-chi/jwtauth/v5 v5.1.1/go.mod h1:CYP1WSbzD4MPuKCr537EM3kfFhSQgpUEtMJFuYJjqWU=
+github.com/go-chi/jwtauth/v5 v5.3.2 h1:s+ON3ATyyMs3Me0kqyuua6Rwu+2zqIIkL0GCaMarwvs=
+github.com/go-chi/jwtauth/v5 v5.3.2/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
diff --git a/src/components/SettingsDialog/Appearance/__tests__/__snapshots__/Appearance.test.tsx.snap b/src/components/SettingsDialog/Appearance/__tests__/__snapshots__/Appearance.test.tsx.snap
index 0549d64105..6ab2868bf4 100644
--- a/src/components/SettingsDialog/Appearance/__tests__/__snapshots__/Appearance.test.tsx.snap
+++ b/src/components/SettingsDialog/Appearance/__tests__/__snapshots__/Appearance.test.tsx.snap
@@ -110,24 +110,6 @@ exports[`Appearance should render all Settings correctly 1`] = `
-
-
-