From 97dc027bb6d3bce7392679c4a3af60955d6fb348 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Fri, 7 Jul 2023 10:01:51 +0400
Subject: [PATCH 1/6] JWT does not need session for CSRF
---
auth.go | 240 ++++++++++++++++++++++++++------------------------
check_csrf.go | 3 +
2 files changed, 130 insertions(+), 113 deletions(-)
diff --git a/auth.go b/auth.go
index 7258ac2e..eae748cc 100644
--- a/auth.go
+++ b/auth.go
@@ -575,140 +575,154 @@ func getSessionByKey(key string) *Session {
return &s
}
-func getSession(r *http.Request) string {
- key, err := r.Cookie("session")
- if err == nil && key != nil {
- return key.Value
+func getJWT(r *http.Request) string {
+ // JWT
+ if r.Header.Get("Authorization") == "" {
+ return ""
}
- if r.Method == "GET" && r.FormValue("session") != "" {
- return r.FormValue("session")
+ if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer") {
+ return ""
}
- if r.Method != "GET" {
- err := r.ParseMultipartForm(2 << 10)
- if err != nil {
- r.ParseForm()
- }
- if r.FormValue("session") != "" {
- return r.FormValue("session")
- }
+
+ jwt := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
+ jwtParts := strings.Split(jwt, ".")
+
+ if len(jwtParts) != 3 {
+ return ""
}
- // JWT
- if r.Header.Get("Authorization") != "" {
- if strings.HasPrefix(r.Header.Get("Authorization"), "Bearer") {
- jwt := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
- jwtParts := strings.Split(jwt, ".")
- if len(jwtParts) != 3 {
- return ""
- }
+ jHeader, err := base64.RawURLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[0])
+ if err != nil {
+ return ""
+ }
+ jPayload, err := base64.RawURLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[1])
+ if err != nil {
+ return ""
+ }
- jHeader, err := base64.RawURLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[0])
- if err != nil {
- return ""
+ header := map[string]interface{}{}
+ err = json.Unmarshal(jHeader, &header)
+ if err != nil {
+ return ""
+ }
+
+ // Get data from payload
+ payload := map[string]interface{}{}
+ err = json.Unmarshal(jPayload, &payload)
+ if err != nil {
+ return ""
+ }
+
+ // Verify issuer
+ if iss, ok := payload["iss"].(string); ok {
+ if iss != JWTIssuer {
+ accepted := false
+ for _, fiss := range AcceptedJWTIssuers {
+ if fiss == iss {
+ accepted = true
+ break
+ }
}
- jPayload, err := base64.RawURLEncoding.WithPadding(base64.NoPadding).DecodeString(jwtParts[1])
- if err != nil {
+ if !accepted {
return ""
}
+ }
+ } else {
+ return ""
+ }
- header := map[string]interface{}{}
- err = json.Unmarshal(jHeader, &header)
- if err != nil {
- return ""
+ // verify audience
+ if aud, ok := payload["aud"].(string); ok {
+ if aud != JWTIssuer {
+ return ""
+ }
+ } else if aud, ok := payload["aud"].([]string); ok {
+ accepted := false
+ for _, audItem := range aud {
+ if audItem == JWTIssuer {
+ accepted = true
+ break
}
+ }
+ if !accepted {
+ return ""
+ }
+ } else {
+ return ""
+ }
- // Get data from payload
- payload := map[string]interface{}{}
- err = json.Unmarshal(jPayload, &payload)
- if err != nil {
- return ""
- }
+ // if there is no subject, return empty session
+ if _, ok := payload["sub"].(string); !ok {
+ return ""
+ }
- // Verify issuer
- if iss, ok := payload["iss"].(string); ok {
- if iss != JWTIssuer {
- accepted := false
- for _, fiss := range AcceptedJWTIssuers {
- if fiss == iss {
- accepted = true
- break
- }
- }
- if !accepted {
- return ""
- }
- }
- } else {
- return ""
- }
+ sub := payload["sub"].(string)
+ user := User{}
+ Get(&user, "username = ?", sub)
- // verify audience
- if aud, ok := payload["aud"].(string); ok {
- if aud != JWTIssuer {
- return ""
- }
- } else if aud, ok := payload["aud"].([]string); ok {
- accepted := false
- for _, audItem := range aud {
- if audItem == JWTIssuer {
- accepted = true
- break
- }
- }
- if !accepted {
- return ""
- }
- } else {
- return ""
- }
+ if user.ID == 0 {
+ return ""
+ }
- // if there is no subject, return empty session
- if _, ok := payload["sub"].(string); !ok {
- return ""
- }
+ session := user.GetActiveSession()
+ if session == nil {
+ return ""
+ }
- sub := payload["sub"].(string)
- user := User{}
- Get(&user, "username = ?", sub)
+ // TODO: verify exp
- if user.ID == 0 {
- return ""
- }
+ // Verify the signature
+ alg := "HS256"
+ if v, ok := header["alg"].(string); ok {
+ alg = v
+ }
+ if _, ok := header["typ"]; ok {
+ if v, ok := header["typ"].(string); !ok || v != "JWT" {
+ return ""
+ }
+ }
+ switch alg {
+ case "HS256":
+ // TODO: allow third party JWT signature authentication
+ hash := hmac.New(sha256.New, []byte(JWT+session.Key))
+ hash.Write([]byte(jwtParts[0] + "." + jwtParts[1]))
+ token := hash.Sum(nil)
+ b64Token := base64.RawURLEncoding.EncodeToString(token)
+ if b64Token != jwtParts[2] {
+ return ""
+ }
+ default:
+ // For now, only support HMAC-SHA256
+ return ""
+ }
+ return session.Key
- session := user.GetActiveSession()
- if session == nil {
- return ""
- }
+}
- // TODO: verify exp
+func getSession(r *http.Request) string {
+ // First, try JWT
+ if val := getJWT(r); val != "" {
+ return val
+ }
- // Verify the signature
- alg := "HS256"
- if v, ok := header["alg"].(string); ok {
- alg = v
- }
- if _, ok := header["typ"]; ok {
- if v, ok := header["typ"].(string); !ok || v != "JWT" {
- return ""
- }
- }
- switch alg {
- case "HS256":
- // TODO: allow third party JWT signature authentication
- hash := hmac.New(sha256.New, []byte(JWT+session.Key))
- hash.Write([]byte(jwtParts[0] + "." + jwtParts[1]))
- token := hash.Sum(nil)
- b64Token := base64.RawURLEncoding.EncodeToString(token)
- if b64Token != jwtParts[2] {
- return ""
- }
- default:
- // For now, only support HMAC-SHA256
- return ""
- }
- return session.Key
+ // Then try session
+ key, err := r.Cookie("session")
+ if err == nil && key != nil {
+ return key.Value
+ }
+ if r.Method == "GET" && r.FormValue("session") != "" {
+ return r.FormValue("session")
+ }
+ if r.Method != "GET" {
+ err := r.ParseMultipartForm(2 << 10)
+ if err != nil {
+ r.ParseForm()
+ }
+ if r.FormValue("session") != "" {
+ return r.FormValue("session")
}
}
+
return ""
}
diff --git a/check_csrf.go b/check_csrf.go
index fcb45f45..5036244f 100644
--- a/check_csrf.go
+++ b/check_csrf.go
@@ -47,6 +47,9 @@ work, `x-csrf-token` parameter should be added.
Where you replace `MY_SESSION_KEY` with the session key.
*/
func CheckCSRF(r *http.Request) bool {
+ if getJWT(r) != "" {
+ return false
+ }
token := getCSRFToken(r)
if token != "" && token == getSession(r) {
return false
From 71ff8369060798d3e91d0fe9608351ca50114dc8 Mon Sep 17 00:00:00 2001
From: Abdullah Alrasheed
Date: Sat, 8 Jul 2023 21:17:38 +0400
Subject: [PATCH 2/6] OpenID Connect for SSO
---
auth.go | 249 +++++++++++++++++--
d_api_auth.go | 4 +
d_api_openid_cert_handler.go | 45 ++++
d_api_openid_login.go | 71 ++++++
global.go | 3 +
go.mod | 1 +
go.sum | 2 +
login_handler.go | 7 +
main_handler.go | 5 +
openid_config_handler.go | 50 ++++
register.go | 4 +
templates/uadmin/default/login.html | 11 +
templates/uadmin/default/openid_concent.html | 79 ++++++
13 files changed, 516 insertions(+), 15 deletions(-)
create mode 100644 d_api_openid_cert_handler.go
create mode 100644 d_api_openid_login.go
create mode 100644 openid_config_handler.go
create mode 100644 templates/uadmin/default/openid_concent.html
diff --git a/auth.go b/auth.go
index eae748cc..8a57fa52 100644
--- a/auth.go
+++ b/auth.go
@@ -4,12 +4,16 @@ import (
"context"
"encoding/base64"
"encoding/json"
+ "fmt"
+ "io"
"math/big"
"net"
+ "os"
"path"
"crypto/hmac"
"crypto/rand"
+ "crypto/rsa"
"crypto/sha256"
"math"
"net/http"
@@ -17,6 +21,7 @@ import (
"strings"
"time"
+ "github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
)
@@ -36,6 +41,8 @@ var JWT = ""
// used to identify the as JWT audience.
var JWTIssuer = ""
+var JWTAlgo = "HS256" //"RS256"
+
// AcceptedJWTIssuers is a list of accepted JWT issuers. By default the
// local JWTIssuer is accepted. To accept other issuers, add them to
// this list
@@ -157,15 +164,20 @@ func createJWT(r *http.Request, s *Session) string {
if !isValidSession(r, s) {
return ""
}
+ alg := JWTAlgo
+ aud := JWTIssuer
+ if r.Context().Value(CKey("aud")) != nil {
+ aud = r.Context().Value(CKey("aud")).(string)
+ }
header := map[string]interface{}{
- "alg": "HS256",
+ "alg": alg,
"typ": "JWT",
}
payload := map[string]interface{}{
"sub": s.User.Username,
"iat": s.LastLogin.Unix(),
"iss": JWTIssuer,
- "aud": JWTIssuer,
+ "aud": aud,
}
if s.ExpiresOn != nil {
payload["exp"] = s.ExpiresOn.Unix()
@@ -176,16 +188,44 @@ func createJWT(r *http.Request, s *Session) string {
payload = CustomJWT(r, s, payload)
}
- jHeader, _ := json.Marshal(header)
- jPayload, _ := json.Marshal(payload)
- b64Header := base64.RawURLEncoding.EncodeToString(jHeader)
- b64Payload := base64.RawURLEncoding.EncodeToString(jPayload)
+ if alg == "HS256" {
+ jHeader, _ := json.Marshal(header)
+ jPayload, _ := json.Marshal(payload)
+ b64Header := base64.RawURLEncoding.EncodeToString(jHeader)
+ b64Payload := base64.RawURLEncoding.EncodeToString(jPayload)
+
+ hash := hmac.New(sha256.New, []byte(JWT+s.Key))
+ hash.Write([]byte(b64Header + "." + b64Payload))
+ signature := hash.Sum(nil)
+ b64Signature := base64.RawURLEncoding.EncodeToString(signature)
+ return b64Header + "." + b64Payload + "." + b64Signature
+ } else if alg == "RS256" {
+ buf, err := os.ReadFile(".jwt-rsa-private.pem")
+ if err != nil {
+ return ""
+ }
+ key, err := jwt.ParseRSAPrivateKeyFromPEM(buf)
+ if err != nil {
+ return ""
+ }
+ header["kid"] = "1"
+ token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims(payload))
+
+ for k, v := range header {
+ token.Header[k] = v
+ }
+
+ tokenRaw, err := token.SignedString(key)
+
+ if err != nil {
+ return ""
+ }
+ return tokenRaw
+ } else {
+ Trail(ERROR, "Unknown algorithm for JWT (%s)", alg)
+ return ""
+ }
- hash := hmac.New(sha256.New, []byte(JWT+s.Key))
- hash.Write([]byte(b64Header + "." + b64Payload))
- signature := hash.Sum(nil)
- b64Signature := base64.RawURLEncoding.EncodeToString(signature)
- return b64Header + "." + b64Payload + "." + b64Signature
}
func isValidSession(r *http.Request, s *Session) bool {
@@ -584,8 +624,8 @@ func getJWT(r *http.Request) string {
return ""
}
- jwt := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
- jwtParts := strings.Split(jwt, ".")
+ jwtToken := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
+ jwtParts := strings.Split(jwtToken, ".")
if len(jwtParts) != 3 {
return ""
@@ -614,10 +654,12 @@ func getJWT(r *http.Request) string {
}
// Verify issuer
+ SSOLogin := false
if iss, ok := payload["iss"].(string); ok {
if iss != JWTIssuer {
accepted := false
for _, fiss := range AcceptedJWTIssuers {
+ Trail(DEBUG, "fiss:%s, iss:%s", fiss, iss)
if fiss == iss {
accepted = true
break
@@ -626,6 +668,7 @@ func getJWT(r *http.Request) string {
if !accepted {
return ""
}
+ SSOLogin = true
}
} else {
return ""
@@ -660,12 +703,31 @@ func getJWT(r *http.Request) string {
user := User{}
Get(&user, "username = ?", sub)
- if user.ID == 0 {
+ if user.ID == 0 && SSOLogin {
+ user := User{
+ Username: sub,
+ FirstName: sub,
+ Active: true,
+ Admin: true,
+ RemoteAccess: true,
+ Password: GenerateBase64(64),
+ }
+ user.Save()
+ } else if user.ID == 0 {
return ""
}
session := user.GetActiveSession()
- if session == nil {
+ if session == nil && SSOLogin {
+ session = &Session{
+ UserID: user.ID,
+ Active: true,
+ LoginTime: time.Now(),
+ IP: GetRemoteIP(r),
+ }
+ session.GenerateKey()
+ session.Save()
+ } else if session == nil {
return ""
}
@@ -681,6 +743,7 @@ func getJWT(r *http.Request) string {
return ""
}
}
+ // verify signature
switch alg {
case "HS256":
// TODO: allow third party JWT signature authentication
@@ -691,20 +754,176 @@ func getJWT(r *http.Request) string {
if b64Token != jwtParts[2] {
return ""
}
+ case "RS256":
+ if !verifyRSA(jwtToken, SSOLogin) {
+ return ""
+ }
default:
// For now, only support HMAC-SHA256
return ""
}
+
return session.Key
}
+var jwtIssuerCerts = map[[2]string][]byte{}
+
+func getJWTRSAPublicKeySSO(jwtToken *jwt.Token) *rsa.PublicKey {
+ iss, err := jwtToken.Claims.GetIssuer()
+ if err != nil {
+ return nil
+ }
+
+ kid, _ := jwtToken.Header["kid"].(string)
+ if kid == "" {
+ return nil
+ }
+
+ if val, ok := jwtIssuerCerts[[2]string{iss, kid}]; ok {
+ cert, _ := jwt.ParseRSAPublicKeyFromPEM(val)
+ return cert
+ }
+
+ res, err := http.Get(iss + "/.well-known/openid-configuration")
+ if err != nil {
+ return nil
+ }
+
+ if res.StatusCode != 200 {
+ return nil
+ }
+
+ buf, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil
+ }
+
+ obj := map[string]interface{}{}
+ err = json.Unmarshal(buf, &obj)
+ if err != nil {
+ return nil
+ }
+
+ crtURL := ""
+ if val, ok := obj["jwks_uri"].(string); !ok || val == "" {
+ return nil
+ } else {
+ crtURL = val
+ }
+
+ res, err = http.Get(crtURL)
+ if err != nil {
+ return nil
+ }
+
+ if res.StatusCode != 200 {
+ return nil
+ }
+
+ buf, err = io.ReadAll(res.Body)
+ if err != nil {
+ return nil
+ }
+
+ certObj := map[string][]map[string]string{}
+ err = json.Unmarshal(buf, &certObj)
+ if err != nil {
+ return nil
+ }
+
+ if val, ok := certObj["keys"]; !ok || len(val) == 0 {
+ return nil
+ }
+
+ var cert map[string]string
+ for i := range certObj["keys"] {
+ if certObj["keys"][i]["kid"] == kid {
+ cert = certObj["keys"][i]
+ break
+ }
+ }
+
+ if cert == nil {
+ return nil
+ }
+
+ N := new(big.Int)
+ buf, _ = base64.RawURLEncoding.DecodeString(cert["n"])
+ N = N.SetBytes(buf)
+
+ E := new(big.Int)
+ buf, _ = base64.RawURLEncoding.DecodeString(cert["e"])
+ E = E.SetBytes(buf)
+ publicCert := rsa.PublicKey{
+ N: N,
+ E: int(E.Int64()),
+ }
+
+ Trail(DEBUG, publicCert)
+
+ return &publicCert
+}
+
+func getJWTRSAPublicKeyLocal(jwtToken *jwt.Token) *rsa.PublicKey {
+ pubKeyPEM, err := os.ReadFile(".jwt-rsa-public.pem")
+ if err != nil {
+ return nil
+ }
+
+ pubKey, err := jwt.ParseRSAPublicKeyFromPEM(pubKeyPEM)
+ if err != nil {
+ return nil
+ }
+
+ return pubKey
+}
+
+func verifyRSA(token string, SSOLogin bool) bool {
+ tok, err := jwt.Parse(token, func(jwtToken *jwt.Token) (interface{}, error) {
+ if _, ok := jwtToken.Method.(*jwt.SigningMethodRSA); !ok {
+ return nil, fmt.Errorf("unexpected method: %s", jwtToken.Header["alg"])
+ }
+
+ var pubKey *rsa.PublicKey
+
+ if SSOLogin {
+ pubKey = getJWTRSAPublicKeySSO(jwtToken)
+ } else {
+ pubKey = getJWTRSAPublicKeyLocal(jwtToken)
+ }
+
+ if pubKey == nil {
+ return nil, fmt.Errorf("Unable to load local public key")
+ }
+
+ return pubKey, nil
+ })
+ if err != nil {
+ return false
+ }
+
+ _, ok := tok.Claims.(jwt.MapClaims)
+ if !ok || !tok.Valid {
+ return false
+ }
+
+ return true
+}
+
func getSession(r *http.Request) string {
// First, try JWT
if val := getJWT(r); val != "" {
return val
}
+ if r.URL.Query().Get("access-token") != "" {
+ r.Header.Add("Authorization", "Bearer "+r.URL.Query().Get("access-token"))
+ if val := getJWT(r); val != "" {
+ return val
+ }
+ }
+
// Then try session
key, err := r.Cookie("session")
if err == nil && key != nil {
diff --git a/d_api_auth.go b/d_api_auth.go
index 6481486c..02c2af2d 100644
--- a/d_api_auth.go
+++ b/d_api_auth.go
@@ -31,6 +31,10 @@ func dAPIAuthHandler(w http.ResponseWriter, r *http.Request, s *Session) {
dAPIResetPasswordHandler(w, r, s)
case "changepassword":
dAPIChangePasswordHandler(w, r, s)
+ case "openidlogin":
+ dAPIOpenIDLoginHandler(w, r, s)
+ case "certs":
+ dAPIOpenIDCertHandler(w, r)
default:
w.WriteHeader(http.StatusNotFound)
ReturnJSON(w, r, map[string]interface{}{
diff --git a/d_api_openid_cert_handler.go b/d_api_openid_cert_handler.go
new file mode 100644
index 00000000..9b82e788
--- /dev/null
+++ b/d_api_openid_cert_handler.go
@@ -0,0 +1,45 @@
+package uadmin
+
+import (
+ "encoding/base64"
+ "math/big"
+ "net/http"
+ "os"
+
+ "github.com/golang-jwt/jwt/v5"
+)
+
+func dAPIOpenIDCertHandler(w http.ResponseWriter, r *http.Request) {
+ buf, err := os.ReadFile(".jwt-rsa-public.pem")
+ if err != nil {
+ w.WriteHeader(404)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "error",
+ "err_msg": "Unable to load public certificate",
+ })
+ return
+ }
+ cert, err := jwt.ParseRSAPublicKeyFromPEM(buf)
+ if err != nil {
+ w.WriteHeader(404)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "error",
+ "err_msg": "Unable to parse public certificate",
+ })
+ return
+ }
+ obj := map[string][]map[string]string{
+ "keys": {
+ {
+ "kid": "1",
+ "use": "sig",
+ "kty": "RSA",
+ "alg": "RS256",
+ "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(cert.E)).Bytes()),
+ "n": base64.RawURLEncoding.EncodeToString(cert.N.Bytes()),
+ },
+ },
+ }
+
+ ReturnJSON(w, r, obj)
+}
diff --git a/d_api_openid_login.go b/d_api_openid_login.go
new file mode 100644
index 00000000..50bbc067
--- /dev/null
+++ b/d_api_openid_login.go
@@ -0,0 +1,71 @@
+package uadmin
+
+import (
+ "context"
+ "net/http"
+ "strings"
+)
+
+func dAPIOpenIDLoginHandler(w http.ResponseWriter, r *http.Request, s *Session) {
+ _ = s
+ redirectURI := r.FormValue("redirect_uri")
+
+ if r.Method == "GET" {
+ if session := IsAuthenticated(r); session != nil {
+ Preload(session, "User")
+ c := map[string]interface{}{
+ "SiteName": SiteName,
+ "Language": getLanguage(r),
+ "RootURL": RootURL,
+ "Logo": Logo,
+ "user": s.User,
+ "OpenIDWebsiteURL": redirectURI,
+ }
+ RenderHTML(w, r, "./templates/uadmin/"+Theme+"/openid_concent.html", c)
+ return
+ }
+
+ http.Redirect(w, r, RootURL+"login/?next="+RootURL+"api/d/auth/openidlogin?"+r.URL.Query().Encode(), 303)
+ return
+ }
+
+ if s == nil {
+ w.WriteHeader(http.StatusUnauthorized)
+ ReturnJSON(w, r, map[string]interface{}{
+ "status": "error",
+ "err_msg": "Invalid credentials",
+ })
+ return
+ }
+
+ // Preload the user to get the group name
+ Preload(&s.User)
+
+ ctx := context.WithValue(r.Context(), CKey("aud"), getAUD(redirectURI))
+ r = r.WithContext(ctx)
+ jwt := createJWT(r, s)
+
+ http.Redirect(w, r, redirectURI+"?access-token="+jwt, 303)
+
+}
+
+func getAUD(URL string) string {
+ aud := ""
+
+ if strings.HasPrefix(URL, "https://") {
+ aud = "https://"
+ URL = strings.TrimPrefix(URL, "https://")
+ }
+
+ if strings.HasPrefix(URL, "http://") {
+ aud = "http://"
+ URL = strings.TrimPrefix(URL, "http://")
+ }
+
+ if strings.Contains(URL, "/") {
+ URL = URL[:strings.Index(URL, "/")]
+ aud += URL
+ }
+
+ return aud
+}
diff --git a/global.go b/global.go
index 16497d76..e923b301 100644
--- a/global.go
+++ b/global.go
@@ -487,6 +487,9 @@ var CompressJSON = false
// CompressJSON is a variable that allows the user to reduce the size of JSON responses
var RemoveZeroValueJSON = false
+// SSOURL enables SSO using OpenID Connect
+var SSOURL = ""
+
// Private Global Variables
// Regex
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
diff --git a/go.mod b/go.mod
index 4c2ac87f..f9aa324a 100644
--- a/go.mod
+++ b/go.mod
@@ -21,6 +21,7 @@ require (
require (
github.com/boombuler/barcode v1.0.1 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
+ github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.3.0 // indirect
diff --git a/go.sum b/go.sum
index 9a1dc4dc..5dea4bf6 100644
--- a/go.sum
+++ b/go.sum
@@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
+github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
diff --git a/login_handler.go b/login_handler.go
index 19053791..7173dcfc 100644
--- a/login_handler.go
+++ b/login_handler.go
@@ -19,6 +19,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
Password string
Logo string
FavIcon string
+ SSOURL string
}
c := Context{}
@@ -27,6 +28,12 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
c.Language = getLanguage(r)
c.Logo = Logo
c.FavIcon = FavIcon
+ c.SSOURL = SSOURL
+
+ if session := IsAuthenticated(r); session != nil {
+ session = session.User.GetActiveSession()
+ SetSessionCookie(w, r, session)
+ }
if r.Method == cPOST {
if r.FormValue("save") == "Send Request" {
diff --git a/main_handler.go b/main_handler.go
index e17bef0e..3b83a637 100644
--- a/main_handler.go
+++ b/main_handler.go
@@ -41,6 +41,7 @@ func mainHandler(w http.ResponseWriter, r *http.Request) {
// This session is preloaded with a user
session := IsAuthenticated(r)
if session == nil {
+ Trail(DEBUG, "no auth, Login page")
loginHandler(w, r)
return
}
@@ -80,6 +81,10 @@ func mainHandler(w http.ResponseWriter, r *http.Request) {
settingsHandler(w, r, session)
return
}
+ if URLParts[0] == "login" {
+ loginHandler(w, r)
+ return
+ }
listHandler(w, r, session)
return
} else if len(URLParts) == 2 {
diff --git a/openid_config_handler.go b/openid_config_handler.go
new file mode 100644
index 00000000..f22d2e4a
--- /dev/null
+++ b/openid_config_handler.go
@@ -0,0 +1,50 @@
+package uadmin
+
+import "net/http"
+
+func JWTConfigHandler(w http.ResponseWriter, r *http.Request) {
+ data := map[string]interface{}{
+ "issuer": JWTIssuer,
+ "authorization_endpoint": JWTIssuer + "/api/d/auth/openidlogin",
+ "token_endpoint": "",
+ "userinfo_endpoint": JWTIssuer + "/api/d/auth/userinfo",
+ "jwks_uri": JWTIssuer + "/api/d/auth/certs",
+ "scopes_supported": []string{
+ "openid",
+ "email",
+ "profile",
+ },
+ "response_types_supported": []string{
+ "code",
+ "token",
+ "id_token",
+ "code token",
+ "code id_token",
+ "token id_token",
+ "code token id_token",
+ "none",
+ },
+ "subject_types_supported": []string{
+ "public",
+ },
+ "id_token_signing_alg_values_supported": []string{
+ "RS256",
+ },
+ "claims_supported": []string{
+ "aud",
+ "email",
+ "email_verified",
+ "exp",
+ "family_name",
+ "given_name",
+ "iat",
+ "iss",
+ "locale",
+ "name",
+ "picture",
+ "sub",
+ },
+ }
+
+ ReturnJSON(w, r, data)
+}
diff --git a/register.go b/register.go
index fa7586c1..a90d7bd4 100644
--- a/register.go
+++ b/register.go
@@ -382,5 +382,9 @@ func registerHandlers() {
http.HandleFunc(RootURL+"api/", Handler(apiHandler))
}
+ if !DisableDAPIAuth {
+ http.HandleFunc(RootURL+".well-known/openid-configuration/", Handler(JWTConfigHandler))
+ }
+
handlersRegistered = true
}
diff --git a/templates/uadmin/default/login.html b/templates/uadmin/default/login.html
index dca60b1b..76ca0f0d 100644
--- a/templates/uadmin/default/login.html
+++ b/templates/uadmin/default/login.html
@@ -95,6 +95,7 @@ {{Tf "uadmin/system" .La
Forgot Password
+ {{if .SSOURL}}SSO Login{{end}}
{{if .ErrExists}}
@@ -150,6 +151,16 @@
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+ Click Continue
+
+
+ to login to {{.OpenIDWebsiteURL}} as {{.user.Username}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+