diff --git a/.gitignore b/.gitignore index f9970e1..aa20365 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,7 @@ drive-service-account.json # Go workspace file go.work -coverage.html \ No newline at end of file +coverage.html + +jwtRS256.key +jwtRS256.key.pub \ No newline at end of file diff --git a/config/config.go b/config/config.go index 69d7eb1..3820eea 100644 --- a/config/config.go +++ b/config/config.go @@ -1,8 +1,11 @@ package config -import "os" +import ( + "crypto/rsa" + "os" +) -var Version = "3.2.13" +var Version = "3.3.0" var Env = os.Getenv("ENV") var Port = os.Getenv("PORT") var Prefix = os.Getenv("PREFIX") @@ -38,6 +41,13 @@ var SubteamRoleNames = []string{"Aero", "Business", "Chassis", "Data", "Electron var AuthSigningKey = os.Getenv("AUTH_SIGNING_KEY") +var RsaPublicKey *rsa.PublicKey +var RsaPrivateKey *rsa.PrivateKey +var RsaPublicKeyJWKS map[string]interface{} + +var RsaPublicKeyString = os.Getenv("RSA_PUBLIC_KEY") +var RsaPrivateKeyString = os.Getenv("RSA_PRIVATE_KEY") + var MemberDirectorySheetID = "1reuLZox2daj8r2H-lZrwB4oFPYlJ6oC7983UUaZd6AY" var DriveCron = os.Getenv("DRIVE_CRON") diff --git a/controller/auth_controller.go b/controller/auth_controller.go index 48d825d..3a5017a 100644 --- a/controller/auth_controller.go +++ b/controller/auth_controller.go @@ -2,6 +2,7 @@ package controller import ( "net/http" + "sentinel/config" "sentinel/model" "sentinel/service" "sentinel/utils" @@ -9,6 +10,10 @@ import ( "github.com/gin-gonic/gin" ) +func GetJWKS(c *gin.Context) { + c.JSON(http.StatusOK, config.RsaPublicKeyJWKS) +} + func RegisterAccountPassword(c *gin.Context) { RequireAny(c, RequestTokenHasScope(c, "sentinel:all")) diff --git a/controller/route_controller.go b/controller/route_controller.go index ec79b34..92e2d4b 100644 --- a/controller/route_controller.go +++ b/controller/route_controller.go @@ -31,6 +31,7 @@ func SetupRouter() *gin.Engine { func InitializeRoutes(router *gin.Engine) { router.GET("/ping", Ping) + router.GET("/auth/keys.json", GetJWKS) router.POST("/auth/register", RegisterAccountPassword) router.POST("/auth/login", LoginAccount) router.POST("/auth/login/discord", LoginDiscord) diff --git a/main.go b/main.go index 474937b..1cdbe93 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ func main() { defer utils.Logger.Sync() database.InitializeDB() + service.InitializeKeys() service.InitializeDrive() service.ConnectDiscord() service.InitializeRoles() @@ -25,7 +26,6 @@ func main() { controller.RegisterDriveCronJob() controller.RegisteGithubCronJob() controller.RegisterWikiCronJob() - // service.FindAllNonVerifiedUsers() router := controller.SetupRouter() controller.InitializeRoutes(router) diff --git a/model/oauth.go b/model/oauth.go index 3f0b262..75230b9 100644 --- a/model/oauth.go +++ b/model/oauth.go @@ -5,6 +5,9 @@ import ( ) var ValidOauthScopes = map[string]string{ + "openid": "OpenID Connect scope", + "profile": "OIDC profile scope", + "email": "OIDC email scope", "user:read": "Read user account information", "user:write": "Edit user account information", "drive:read": "Read user's team drive access information", diff --git a/scripts/keygen.sh b/scripts/keygen.sh new file mode 100755 index 0000000..6e80a7b --- /dev/null +++ b/scripts/keygen.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key +openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub + +echo "RSA_PUBLIC_KEY=\"$(awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' jwtRS256.key.pub)\"" +echo "RSA_PRIVATE_KEY=\"$(awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' jwtRS256.key)\"" + +# Clean up temporary files +rm jwtRS256.key jwtRS256.key.pub diff --git a/service/auth_service.go b/service/auth_service.go index be72f10..f99d250 100644 --- a/service/auth_service.go +++ b/service/auth_service.go @@ -1,7 +1,10 @@ package service import ( + "crypto/rsa" + "encoding/base64" "fmt" + "math/big" "sentinel/config" "sentinel/database" "sentinel/model" @@ -14,6 +17,40 @@ import ( "golang.org/x/crypto/bcrypt" ) +func InitializeKeys() { + // Parse the RSA public key + publicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(config.RsaPublicKeyString)) + if err != nil { + utils.SugarLogger.Errorln("Failed to parse RSA public key:", err) + } + config.RsaPublicKey = publicKey + // Parse the RSA private key + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(config.RsaPrivateKeyString)) + if err != nil { + utils.SugarLogger.Errorln("Failed to parse RSA private key:", err) + } + config.RsaPrivateKey = privateKey + config.RsaPublicKeyJWKS = PublicKeyToJWKS(publicKey) +} + +func PublicKeyToJWKS(publicKey *rsa.PublicKey) map[string]interface{} { + e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(publicKey.E)).Bytes()) + n := base64.RawURLEncoding.EncodeToString(publicKey.N.Bytes()) + + return map[string]interface{}{ + "keys": []map[string]interface{}{ + { + "kty": "RSA", + "use": "sig", + "alg": "RS256", + "kid": "1", + "n": n, + "e": e, + }, + }, + } +} + func RegisterEmailPassword(email string, password string) (string, error) { user := GetUserByEmail(email) if user.ID == "" { @@ -97,15 +134,15 @@ func GenerateJWT(id string, email string, scope string, client_id string) (strin Scope: scope, RegisteredClaims: jwt.RegisteredClaims{ ID: id, - Issuer: "sso.gauchoracing.com", + Issuer: "https://sso.gauchoracing.com/", Audience: jwt.ClaimStrings{client_id}, IssuedAt: jwt.NewNumericDate(time.Now()), ExpiresAt: jwt.NewNumericDate(expirationTime), }, } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signedToken, err := token.SignedString([]byte(config.AuthSigningKey)) + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + signedToken, err := token.SignedString(config.RsaPrivateKey) if err != nil { utils.SugarLogger.Errorln(err.Error()) return "", err @@ -116,7 +153,7 @@ func GenerateJWT(id string, email string, scope string, client_id string) (strin func ValidateJWT(token string) (*model.AuthClaims, error) { claims := &model.AuthClaims{} _, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) { - return []byte(config.AuthSigningKey), nil + return config.RsaPublicKey, nil }) if err != nil { utils.SugarLogger.Errorln(err.Error()) diff --git a/service/oauth_service.go b/service/oauth_service.go index 9aa26c0..cbef2b3 100644 --- a/service/oauth_service.go +++ b/service/oauth_service.go @@ -137,11 +137,19 @@ func ValidateScope(scopes string) bool { func GenerateAuthorizationCode(clientID, userID, scope string) (model.AuthorizationCode, error) { code := generateCryptoString(8) expiresAt := time.Now().Add(5 * time.Minute) + + // Check if scope contains "oidc" and add "user:read" if it does + scopes := strings.Split(scope, " ") + if contains(scopes, "oidc") && !contains(scopes, "user:read") { + scopes = append(scopes, "user:read") + } + updatedScope := strings.Join(scopes, " ") + authCode := model.AuthorizationCode{ Code: code, ClientID: clientID, UserID: userID, - Scope: scope, + Scope: updatedScope, ExpiresAt: utils.WithPrecision(expiresAt), } result := database.DB.Create(&authCode) diff --git a/utils/config.go b/utils/config.go index ba48e91..dfa76f6 100644 --- a/utils/config.go +++ b/utils/config.go @@ -1,6 +1,9 @@ package utils -import "sentinel/config" +import ( + "sentinel/config" + "strings" +) func VerifyConfig() { if config.Port == "" { @@ -44,6 +47,14 @@ func VerifyConfig() { if config.AuthSigningKey == "" { SugarLogger.Errorf("AUTH_SIGNING_KEY is not set") } + if config.RsaPublicKeyString == "" { + SugarLogger.Errorf("RSA_PUBLIC_KEY is not set") + } + config.RsaPublicKeyString = strings.ReplaceAll(config.RsaPublicKeyString, "\\n", "\n") + if config.RsaPrivateKeyString == "" { + SugarLogger.Errorf("RSA_PRIVATE_KEY is not set") + } + config.RsaPrivateKeyString = strings.ReplaceAll(config.RsaPrivateKeyString, "\\n", "\n") if config.DriveCron == "" { config.DriveCron = "0 * * * *" SugarLogger.Infof("DRIVE_CRON is not set, defaulting to %s", config.DriveCron)