mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-03-01 04:11:58 +00:00
feat: jwk endpoint
This commit is contained in:
@@ -27,7 +27,7 @@ export default defineConfig({
|
|||||||
"/.well-known": {
|
"/.well-known": {
|
||||||
target: "http://tinyauth-backend:3000/.well-known",
|
target: "http://tinyauth-backend:3000/.well-known",
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/.well-known/, ""),
|
rewrite: (path) => path.replace(/^\/\.well-known/, ""),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
allowedHosts: true,
|
allowedHosts: true,
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -61,6 +61,7 @@ require (
|
|||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -103,6 +103,8 @@ github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
|||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
|||||||
@@ -113,9 +113,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
|
|
||||||
healthController.SetupRoutes()
|
healthController.SetupRoutes()
|
||||||
|
|
||||||
wellknownController := controller.NewWellKnownController(controller.WellKnownControllerConfig{
|
wellknownController := controller.NewWellKnownController(controller.WellKnownControllerConfig{}, app.services.oidcService, engine)
|
||||||
OpenIDConnectIssuer: app.services.oidcService.GetIssuer(),
|
|
||||||
}, engine)
|
|
||||||
|
|
||||||
wellknownController.SetupRoutes()
|
wellknownController.SetupRoutes()
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We also need a snapshot of the user that authorized this (skip if no openid scope)
|
// We also need a snapshot of the user that authorized this (skip if no openid scope)
|
||||||
if slices.Contains(strings.Split(req.Scope, " "), "openid") {
|
if slices.Contains(strings.Fields(req.Scope), "openid") {
|
||||||
err = controller.oidc.StoreUserinfo(c, sub, userContext, req)
|
err = controller.oidc.StoreUserinfo(c, sub, userContext, req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/steveiliop56/tinyauth/internal/service"
|
"github.com/steveiliop56/tinyauth/internal/service"
|
||||||
@@ -23,33 +24,35 @@ type OpenIDConnectConfiguration struct {
|
|||||||
ServiceDocumentation string `json:"service_documentation"`
|
ServiceDocumentation string `json:"service_documentation"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WellKnownControllerConfig struct {
|
type WellKnownControllerConfig struct{}
|
||||||
OpenIDConnectIssuer string
|
|
||||||
}
|
|
||||||
|
|
||||||
type WellKnownController struct {
|
type WellKnownController struct {
|
||||||
config WellKnownControllerConfig
|
config WellKnownControllerConfig
|
||||||
engine *gin.Engine
|
engine *gin.Engine
|
||||||
|
oidc *service.OIDCService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWellKnownController(config WellKnownControllerConfig, engine *gin.Engine) *WellKnownController {
|
func NewWellKnownController(config WellKnownControllerConfig, oidc *service.OIDCService, engine *gin.Engine) *WellKnownController {
|
||||||
return &WellKnownController{
|
return &WellKnownController{
|
||||||
config: config,
|
config: config,
|
||||||
|
oidc: oidc,
|
||||||
engine: engine,
|
engine: engine,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *WellKnownController) SetupRoutes() {
|
func (controller *WellKnownController) SetupRoutes() {
|
||||||
controller.engine.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration)
|
controller.engine.GET("/.well-known/openid-configuration", controller.OpenIDConnectConfiguration)
|
||||||
|
controller.engine.GET("/.well-known/jwks.json", controller.JWKS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context) {
|
func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context) {
|
||||||
|
issuer := controller.oidc.GetIssuer()
|
||||||
c.JSON(200, OpenIDConnectConfiguration{
|
c.JSON(200, OpenIDConnectConfiguration{
|
||||||
Issuer: controller.config.OpenIDConnectIssuer,
|
Issuer: issuer,
|
||||||
AuthorizationEndpoint: fmt.Sprintf("%s/authorize", controller.config.OpenIDConnectIssuer),
|
AuthorizationEndpoint: fmt.Sprintf("%s/authorize", issuer),
|
||||||
TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", controller.config.OpenIDConnectIssuer),
|
TokenEndpoint: fmt.Sprintf("%s/api/oidc/token", issuer),
|
||||||
UserinfoEndpoint: fmt.Sprintf("%s/api/oidc/userinfo", controller.config.OpenIDConnectIssuer),
|
UserinfoEndpoint: fmt.Sprintf("%s/api/oidc/userinfo", issuer),
|
||||||
JwksUri: fmt.Sprintf("%s/api/oidc/jwks", controller.config.OpenIDConnectIssuer),
|
JwksUri: fmt.Sprintf("%s/.well-known/jwks.json", issuer),
|
||||||
ScopesSupported: service.SupportedScopes,
|
ScopesSupported: service.SupportedScopes,
|
||||||
ResponseTypesSupported: service.SupportedResponseTypes,
|
ResponseTypesSupported: service.SupportedResponseTypes,
|
||||||
GrantTypesSupported: service.SupportedGrantTypes,
|
GrantTypesSupported: service.SupportedGrantTypes,
|
||||||
@@ -60,3 +63,23 @@ func (controller *WellKnownController) OpenIDConnectConfiguration(c *gin.Context
|
|||||||
ServiceDocumentation: "https://tinyauth.app/docs/reference/openid",
|
ServiceDocumentation: "https://tinyauth.app/docs/reference/openid",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (controller *WellKnownController) JWKS(c *gin.Context) {
|
||||||
|
jwks, err := controller.oidc.GetJWK()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{
|
||||||
|
"status": "500",
|
||||||
|
"message": "failed to get JWK",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("content-type", "application/json")
|
||||||
|
|
||||||
|
c.Writer.WriteString(`{"keys":[`)
|
||||||
|
c.Writer.Write(jwks)
|
||||||
|
c.Writer.WriteString(`]}`)
|
||||||
|
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -17,14 +18,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-jose/go-jose/v4"
|
||||||
"github.com/steveiliop56/tinyauth/internal/config"
|
"github.com/steveiliop56/tinyauth/internal/config"
|
||||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils"
|
"github.com/steveiliop56/tinyauth/internal/utils"
|
||||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
|
|
||||||
// Should probably switch to another package but for now this works
|
|
||||||
"golang.org/x/oauth2/jws"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -40,6 +39,14 @@ var (
|
|||||||
ErrTokenExpired = errors.New("token_expired")
|
ErrTokenExpired = errors.New("token_expired")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ClaimSet struct {
|
||||||
|
Iss string `json:"iss"`
|
||||||
|
Aud string `json:"aud"`
|
||||||
|
Sub string `json:"sub"`
|
||||||
|
Iat int64 `json:"iat"`
|
||||||
|
Exp int64 `json:"exp"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserinfoResponse struct {
|
type UserinfoResponse struct {
|
||||||
Sub string `json:"sub"`
|
Sub string `json:"sub"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -333,7 +340,21 @@ func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, sub
|
|||||||
createdAt := time.Now().Unix()
|
createdAt := time.Now().Unix()
|
||||||
expiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
expiresAt := time.Now().Add(time.Duration(service.config.SessionExpiry) * time.Second).Unix()
|
||||||
|
|
||||||
claims := jws.ClaimSet{
|
signer, err := jose.NewSigner(jose.SigningKey{
|
||||||
|
Algorithm: jose.RS256,
|
||||||
|
Key: service.privateKey,
|
||||||
|
}, &jose.SignerOptions{
|
||||||
|
ExtraHeaders: map[jose.HeaderKey]any{
|
||||||
|
"typ": "jwt",
|
||||||
|
"jku": fmt.Sprintf("%s/.well-known/jwks.json", service.issuer),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := ClaimSet{
|
||||||
Iss: service.issuer,
|
Iss: service.issuer,
|
||||||
Aud: client.ClientID,
|
Aud: client.ClientID,
|
||||||
Sub: sub,
|
Sub: sub,
|
||||||
@@ -341,12 +362,19 @@ func (service *OIDCService) generateIDToken(client config.OIDCClientConfig, sub
|
|||||||
Exp: expiresAt,
|
Exp: expiresAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
header := jws.Header{
|
payload, err := json.Marshal(claims)
|
||||||
Algorithm: "RS256",
|
|
||||||
Typ: "JWT",
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := jws.Encode(&header, &claims, service.privateKey)
|
object, err := signer.Sign(payload)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := object.CompactSerialize()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -595,3 +623,13 @@ func (service *OIDCService) Cleanup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (service *OIDCService) GetJWK() ([]byte, error) {
|
||||||
|
jwk := jose.JSONWebKey{
|
||||||
|
Key: service.privateKey,
|
||||||
|
Algorithm: string(jose.RS256),
|
||||||
|
Use: "sig",
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwk.Public().MarshalJSON()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user