Compare commits
2 Commits
078aae8172
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 744caf9a5e | |||
| 979747e288 |
@@ -30,6 +30,15 @@ type Config struct {
|
|||||||
PermissionConnectorPort string
|
PermissionConnectorPort string
|
||||||
PermissionConnectorAdminPort string
|
PermissionConnectorAdminPort string
|
||||||
|
|
||||||
|
// OAuthRedirectURI is the registered OAuth2 redirect_uri (frontend callback URL).
|
||||||
|
// After a successful login, Hydra redirects here with the authorization code.
|
||||||
|
// The original protected URL is passed as the state parameter.
|
||||||
|
AdminOrigin string
|
||||||
|
Origin string
|
||||||
|
|
||||||
|
OAuthRedirectURI string
|
||||||
|
OAdminAuthRedirectURI string
|
||||||
|
|
||||||
Local bool
|
Local bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
oclib "cloud.o-forge.io/core/oc-lib"
|
oclib "cloud.o-forge.io/core/oc-lib"
|
||||||
|
"cloud.o-forge.io/core/oc-lib/dbs"
|
||||||
"cloud.o-forge.io/core/oc-lib/models/peer"
|
"cloud.o-forge.io/core/oc-lib/models/peer"
|
||||||
model "cloud.o-forge.io/core/oc-lib/models/peer"
|
model "cloud.o-forge.io/core/oc-lib/models/peer"
|
||||||
beego "github.com/beego/beego/v2/server/web"
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
@@ -27,6 +28,8 @@ type OAuthController struct {
|
|||||||
// @Title GetLogin
|
// @Title GetLogin
|
||||||
// @Description Hydra redirects here with a login_challenge. Returns challenge info or auto-accepts if session exists.
|
// @Description Hydra redirects here with a login_challenge. Returns challenge info or auto-accepts if session exists.
|
||||||
// @Param login_challenge query string true "The login challenge from Hydra"
|
// @Param login_challenge query string true "The login challenge from Hydra"
|
||||||
|
// @Param redirect query string true "explicit redirect by passed"
|
||||||
|
|
||||||
// @Success 200 {object} auth_connectors.LoginChallenge
|
// @Success 200 {object} auth_connectors.LoginChallenge
|
||||||
// @Failure 400 missing login_challenge
|
// @Failure 400 missing login_challenge
|
||||||
// @Failure 500 internal error
|
// @Failure 500 internal error
|
||||||
@@ -73,8 +76,9 @@ func (o *OAuthController) GetLogin() {
|
|||||||
o.Data["json"] = redirect
|
o.Data["json"] = redirect
|
||||||
o.ServeJSON()
|
o.ServeJSON()
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
// Return challenge info so frontend can render login form
|
// Return challenge info so frontend can render login form
|
||||||
o.Data["json"] = loginChallenge
|
o.Data["json"] = loginChallenge
|
||||||
o.ServeJSON()
|
o.ServeJSON()
|
||||||
@@ -82,13 +86,17 @@ func (o *OAuthController) GetLogin() {
|
|||||||
|
|
||||||
// @Title PostLogin
|
// @Title PostLogin
|
||||||
// @Description Authenticate user via LDAP and accept Hydra login challenge
|
// @Description Authenticate user via LDAP and accept Hydra login challenge
|
||||||
|
// @Param redirect query string true "explicit redirect by passed"
|
||||||
// @Param body body auth_connectors.LoginRequest true "Login credentials and challenge"
|
// @Param body body auth_connectors.LoginRequest true "Login credentials and challenge"
|
||||||
|
|
||||||
// @Success 200 {object} auth_connectors.Redirect
|
// @Success 200 {object} auth_connectors.Redirect
|
||||||
// @Failure 401 invalid credentials
|
// @Failure 401 invalid credentials
|
||||||
// @Failure 500 internal error
|
// @Failure 500 internal error
|
||||||
// @router /login [post]
|
// @router /login [post]
|
||||||
func (o *OAuthController) Login() {
|
func (o *OAuthController) Login() {
|
||||||
logger := oclib.GetLogger()
|
logger := oclib.GetLogger()
|
||||||
|
red := o.Ctx.Input.Query("redirect")
|
||||||
|
|
||||||
var req auth_connectors.LoginRequest
|
var req auth_connectors.LoginRequest
|
||||||
if err := json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &req); err != nil {
|
if err := json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &req); err != nil {
|
||||||
o.Ctx.ResponseWriter.WriteHeader(400)
|
o.Ctx.ResponseWriter.WriteHeader(400)
|
||||||
@@ -159,13 +167,18 @@ func (o *OAuthController) Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return redirect_to so the frontend follows the OAuth2 flow
|
// Return redirect_to so the frontend follows the OAuth2 flow
|
||||||
o.Data["json"] = redirect
|
if red == "false" {
|
||||||
o.ServeJSON()
|
o.Data["json"] = redirect
|
||||||
|
o.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
o.Redirect(redirect.RedirectTo, 303)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Title Consent
|
// @Title Consent
|
||||||
// @Description Hydra redirects here with a consent_challenge. Auto-accepts consent with user permissions.
|
// @Description Hydra redirects here with a consent_challenge. Auto-accepts consent with user permissions.
|
||||||
// @Param consent_challenge query string true "The consent challenge from Hydra"
|
// @Param consent_challenge query string true "The consent challenge from Hydra"
|
||||||
|
// @Param redirect query string true "explicit redirect by passed"
|
||||||
// @Success 200 {object} auth_connectors.Redirect
|
// @Success 200 {object} auth_connectors.Redirect
|
||||||
// @Failure 400 missing consent_challenge
|
// @Failure 400 missing consent_challenge
|
||||||
// @Failure 500 internal error
|
// @Failure 500 internal error
|
||||||
@@ -191,8 +204,13 @@ func (o *OAuthController) Consent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get self peer for signing
|
// Get self peer for signing
|
||||||
pp := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), "", "", []string{}, nil).Search(
|
pp := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil).Search(
|
||||||
nil, strconv.Itoa(peer.SELF.EnumIndex()), false)
|
&dbs.Filters{
|
||||||
|
Or: map[string][]dbs.Filter{ // search by name if no filters are provided
|
||||||
|
"relation": {{Operator: dbs.EQUAL.String(), Value: peer.SELF}},
|
||||||
|
},
|
||||||
|
}, strconv.Itoa(peer.SELF.EnumIndex()), false)
|
||||||
|
fmt.Println(pp.Err, pp.Data)
|
||||||
if len(pp.Data) == 0 || pp.Code >= 300 || pp.Err != "" {
|
if len(pp.Data) == 0 || pp.Code >= 300 || pp.Err != "" {
|
||||||
logger.Error().Msg("Self peer not found")
|
logger.Error().Msg("Self peer not found")
|
||||||
o.Ctx.ResponseWriter.WriteHeader(500)
|
o.Ctx.ResponseWriter.WriteHeader(500)
|
||||||
@@ -231,12 +249,16 @@ func (o *OAuthController) Consent() {
|
|||||||
// @Title GetLogout
|
// @Title GetLogout
|
||||||
// @Description Hydra redirects here with a logout_challenge. Accepts the challenge and returns a redirect URL.
|
// @Description Hydra redirects here with a logout_challenge. Accepts the challenge and returns a redirect URL.
|
||||||
// @Param logout_challenge query string true "The logout challenge from Hydra"
|
// @Param logout_challenge query string true "The logout challenge from Hydra"
|
||||||
|
// @Param redirect query string true "explicit redirect by passed"
|
||||||
|
|
||||||
// @Success 200 {object} auth_connectors.Redirect
|
// @Success 200 {object} auth_connectors.Redirect
|
||||||
// @Failure 400 missing logout_challenge
|
// @Failure 400 missing logout_challenge
|
||||||
// @Failure 500 internal error
|
// @Failure 500 internal error
|
||||||
// @router /logout [get]
|
// @router /logout [get]
|
||||||
func (o *OAuthController) GetLogout() {
|
func (o *OAuthController) GetLogout() {
|
||||||
logger := oclib.GetLogger()
|
logger := oclib.GetLogger()
|
||||||
|
red := o.Ctx.Input.Query("redirect")
|
||||||
|
|
||||||
challenge := o.Ctx.Input.Query("logout_challenge")
|
challenge := o.Ctx.Input.Query("logout_challenge")
|
||||||
if challenge == "" {
|
if challenge == "" {
|
||||||
o.Ctx.ResponseWriter.WriteHeader(400)
|
o.Ctx.ResponseWriter.WriteHeader(400)
|
||||||
@@ -268,15 +290,19 @@ func (o *OAuthController) GetLogout() {
|
|||||||
o.ServeJSON()
|
o.ServeJSON()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if red == "false" {
|
||||||
o.Data["json"] = redirect
|
o.Data["json"] = redirect
|
||||||
o.ServeJSON()
|
o.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
o.Redirect(redirect.RedirectTo, 303)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Title Logout
|
// @Title Logout
|
||||||
// @Description Revoke an OAuth2 token
|
// @Description Revoke an OAuth2 token
|
||||||
// @Param Authorization header string false "Bearer token"
|
// @Param Authorization header string false "Bearer token"
|
||||||
// @Param client_id query string true "The client_id"
|
// @Param client_id query string true "The client_id"
|
||||||
|
|
||||||
// @Success 200 {object} auth_connectors.Token
|
// @Success 200 {object} auth_connectors.Token
|
||||||
// @router /logout [delete]
|
// @router /logout [delete]
|
||||||
func (o *OAuthController) LogOut() {
|
func (o *OAuthController) LogOut() {
|
||||||
@@ -374,48 +400,104 @@ func (o *OAuthController) Introspect() {
|
|||||||
o.ServeJSON()
|
o.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// whitelist lists path segments of oc-auth's own challenge endpoints.
|
||||||
|
// These require no token — the challenge is passed as a query parameter by Hydra.
|
||||||
var whitelist = []string{
|
var whitelist = []string{
|
||||||
|
"/public/",
|
||||||
|
"/version",
|
||||||
|
"/status",
|
||||||
"/login",
|
"/login",
|
||||||
|
"/logout",
|
||||||
"/refresh",
|
"/refresh",
|
||||||
"/introspect",
|
"/introspect",
|
||||||
"/consent",
|
"/consent",
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Title AuthForward
|
// @Title AuthForward
|
||||||
// @Description Forward auth for Traefik — validates JWT via Hydra introspection
|
// @Description Forward auth for Traefik — validates JWT via Hydra introspection.
|
||||||
|
// Only requests from our own peer (SELF) are authorized.
|
||||||
|
// Routes in pathWhitelist bypass all checks (with or without token).
|
||||||
|
// Routes in whitelist bypass the token check (oc-auth own challenge endpoints).
|
||||||
|
// On missing/invalid token: 302 to Hydra authorization URL (restart OAuth2 flow).
|
||||||
|
// On wrong peer: 401 (network/config issue, no redirect).
|
||||||
|
// On valid token but insufficient permissions: 403.
|
||||||
|
// On success: 200 so Traefik forwards the request to the target route.
|
||||||
// @Param Authorization header string false "Bearer token"
|
// @Param Authorization header string false "Bearer token"
|
||||||
// @Success 200 {string}
|
// @Success 200 {string}
|
||||||
// @router /forward [get]
|
// @router /forward [get]
|
||||||
func (o *OAuthController) InternalAuthForward() {
|
func (o *OAuthController) InternalAuthForward() {
|
||||||
|
fmt.Println("InternalAuthForward")
|
||||||
|
uri := o.Ctx.Request.Header.Get("X-Forwarded-Uri")
|
||||||
|
for _, w := range whitelist {
|
||||||
|
if strings.Contains(uri, w) {
|
||||||
|
fmt.Println("WHITELIST", w)
|
||||||
|
o.Ctx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
origin, publicKey, external := o.extractOrigin(o.Ctx.Request)
|
||||||
|
|
||||||
reqToken := o.Ctx.Request.Header.Get("Authorization")
|
reqToken := o.Ctx.Request.Header.Get("Authorization")
|
||||||
if reqToken == "" {
|
if reqToken == "" {
|
||||||
for _, w := range whitelist {
|
// Step 1: no token — allow oc-auth's own challenge endpoints (no token needed).
|
||||||
if strings.Contains(o.Ctx.Request.Header.Get("X-Forwarded-Uri"), w) {
|
// No token and not a whitelisted path → restart OAuth2 flow.
|
||||||
o.Ctx.ResponseWriter.WriteHeader(200)
|
fmt.Println("NO TOKEN")
|
||||||
o.ServeJSON()
|
o.redirectToLogin(origin)
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
o.Ctx.ResponseWriter.WriteHeader(401)
|
|
||||||
o.ServeJSON()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 2: extract Bearer token — malformed header treated as missing token.
|
||||||
splitToken := strings.Split(reqToken, "Bearer ")
|
splitToken := strings.Split(reqToken, "Bearer ")
|
||||||
if len(splitToken) < 2 {
|
if len(splitToken) < 2 || splitToken[1] == "" {
|
||||||
reqToken = ""
|
fmt.Println("MALFORMED BEARER")
|
||||||
} else {
|
o.redirectToLogin(origin)
|
||||||
reqToken = splitToken[1]
|
return
|
||||||
}
|
}
|
||||||
origin, publicKey, external := o.extractOrigin(o.Ctx.Request)
|
reqToken = splitToken[1]
|
||||||
if !infrastructure.GetAuthConnector().CheckAuthForward(
|
|
||||||
|
// Step 3: resolve the calling peer — only our own peer (SELF) is authorized.
|
||||||
|
// A non-SELF or unknown peer is a network/config issue, not a login problem → 401.
|
||||||
|
if external || origin == "" || publicKey == "" {
|
||||||
|
fmt.Println("Unauthorized", external, origin, publicKey)
|
||||||
|
o.Ctx.ResponseWriter.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: introspect via Hydra then check permissions via Keto.
|
||||||
|
// 401 → token inactive/invalid, user must re-authenticate → restart OAuth2 flow.
|
||||||
|
// 403 → token valid, but permissions denied → forbidden.
|
||||||
|
// 200 → all good, let Traefik forward to the target route.
|
||||||
|
switch infrastructure.GetAuthConnector().CheckAuthForward(
|
||||||
reqToken, publicKey, origin,
|
reqToken, publicKey, origin,
|
||||||
o.Ctx.Request.Header.Get("X-Forwarded-Method"),
|
o.Ctx.Request.Header.Get("X-Forwarded-Method"),
|
||||||
o.Ctx.Request.Header.Get("X-Forwarded-Uri"), external) && origin != "" && publicKey != "" {
|
uri, external) {
|
||||||
o.Ctx.ResponseWriter.WriteHeader(401)
|
case http.StatusOK:
|
||||||
o.ServeJSON()
|
fmt.Println("OK")
|
||||||
return
|
o.Ctx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||||
|
case http.StatusForbidden:
|
||||||
|
fmt.Println("StatusForbidden")
|
||||||
|
o.Ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
|
||||||
|
default:
|
||||||
|
fmt.Println("redirectToLogin UNAUTHORIZED")
|
||||||
|
// 401 or unexpected status → token likely expired, restart the OAuth2 flow.
|
||||||
|
o.redirectToLogin(origin)
|
||||||
}
|
}
|
||||||
o.ServeJSON()
|
}
|
||||||
|
|
||||||
|
// redirectToLogin redirects the client to Hydra's authorization endpoint to start a fresh
|
||||||
|
// OAuth2 flow. The original request URL is passed as the state parameter so the frontend
|
||||||
|
// can redirect back after successful authentication.
|
||||||
|
func (o *OAuthController) redirectToLogin(origin string) {
|
||||||
|
cfg := conf.GetConfig()
|
||||||
|
|
||||||
|
if strings.Contains(origin, cfg.AdminOrigin) {
|
||||||
|
o.Ctx.ResponseWriter.Header().Set("Location", cfg.OAdminAuthRedirectURI)
|
||||||
|
} else {
|
||||||
|
o.Ctx.ResponseWriter.Header().Set("Location", cfg.OAuthRedirectURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
o.Ctx.ResponseWriter.WriteHeader(http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OAuthController) extractOrigin(request *http.Request) (string, string, bool) {
|
func (o *OAuthController) extractOrigin(request *http.Request) (string, string, bool) {
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ services:
|
|||||||
container_name: oc-auth
|
container_name: oc-auth
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.auth.entrypoints=web"
|
- "traefik.http.routers.auth-sec.entrypoints=web"
|
||||||
- "traefik.http.routers.auth.rule=PathPrefix(`/auth`)"
|
- "traefik.http.routers.auth-sec.rule=PathPrefix(`/auth/`)"
|
||||||
- "traefik.http.middlewares.auth-rewrite.replacepathregex.regex=^/auth(.*)"
|
- "traefik.http.middlewares.auth-sec-rewrite.replacepathregex.regex=^/auth(.*)"
|
||||||
- "traefik.http.middlewares.auth-rewrite.replacepathregex.replacement=/oc$$1"
|
- "traefik.http.middlewares.auth-sec-rewrite.replacepathregex.replacement=/oc$$1"
|
||||||
- "traefik.http.routers.auth.middlewares=auth-rewrite"
|
- "traefik.http.services.auth-sec.loadbalancer.server.port=8080"
|
||||||
- "traefik.http.services.auth.loadbalancer.server.port=8080"
|
- "traefik.http.routers.auth-sec.middlewares=auth-sec-rewrite,auth-auth-sec"
|
||||||
- "traefik.http.middlewares.auth.forwardauth.address=http://oc-auth:8080/oc/forward"
|
- "traefik.http.middlewares.auth-auth-sec.forwardauth.address=http://oc-auth:8080/oc/forward"
|
||||||
|
- "traefik.http.middlewares.auth-auth-sec.forwardauth.trustForwardHeader=true"
|
||||||
|
- "traefik.http.middlewares.auth-auth-sec.forwardauth.authResponseHeaders=X-Auth-Request-User,X-Auth-Request-Email"
|
||||||
environment:
|
environment:
|
||||||
LDAP_ENDPOINTS: ldap:389
|
LDAP_ENDPOINTS: ldap:389
|
||||||
LDAP_BINDDN: cn=admin,dc=example,dc=com
|
LDAP_BINDDN: cn=admin,dc=example,dc=com
|
||||||
|
|||||||
@@ -27,8 +27,12 @@ type AuthConnector interface {
|
|||||||
RevokeToken(token string, clientID string) error
|
RevokeToken(token string, clientID string) error
|
||||||
RefreshToken(refreshToken string, clientID string) (*TokenResponse, error)
|
RefreshToken(refreshToken string, clientID string) (*TokenResponse, error)
|
||||||
|
|
||||||
// Forward auth
|
// CheckAuthForward validates the token and permissions for a forward auth request.
|
||||||
CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) bool
|
// Returns an HTTP status code:
|
||||||
|
// 200 — token active and permissions granted
|
||||||
|
// 401 — token missing, invalid, or inactive → caller should redirect to login
|
||||||
|
// 403 — token valid but permissions denied → caller should return forbidden
|
||||||
|
CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token is the unified token response returned to clients
|
// Token is the unified token response returned to clients
|
||||||
|
|||||||
@@ -299,61 +299,61 @@ func (h *HydraConnector) RefreshToken(refreshToken string, clientID string) (*To
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckAuthForward validates a JWT token for forward auth (Traefik integration)
|
// CheckAuthForward validates a JWT token for forward auth (Traefik integration).
|
||||||
// It introspects the token via Hydra and checks permissions from the token's extra claims
|
// It introspects the token via Hydra then checks permissions via Keto.
|
||||||
func (h *HydraConnector) CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) bool {
|
// Only requests from our own peer (external == false) are accepted.
|
||||||
|
// Returns 200 (OK), 401 (token inactive/invalid → redirect to login), or 403 (permission denied).
|
||||||
|
func (h *HydraConnector) CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) int {
|
||||||
if forward == "" || method == "" {
|
if forward == "" || method == "" {
|
||||||
return false
|
return http.StatusUnauthorized
|
||||||
|
}
|
||||||
|
// Defense in depth: only SELF peer requests are allowed.
|
||||||
|
if external {
|
||||||
|
return http.StatusUnauthorized
|
||||||
}
|
}
|
||||||
logger := oclib.GetLogger()
|
logger := oclib.GetLogger()
|
||||||
|
|
||||||
// Introspect the token via Hydra to get claims
|
// Introspect the token via Hydra.
|
||||||
|
// An inactive or invalid token means the user must re-authenticate → 401.
|
||||||
result, err := h.Introspect(reqToken)
|
result, err := h.Introspect(reqToken)
|
||||||
if err != nil || !result.Active {
|
if err != nil || !result.Active {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Msg("Forward auth introspect failed: " + err.Error())
|
logger.Error().Msg("Forward auth introspect failed: " + err.Error())
|
||||||
}
|
}
|
||||||
return false
|
return http.StatusUnauthorized
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract claims from the introspection result's extra data
|
// Build session claims from Hydra's introspection "ext" field.
|
||||||
// Hydra puts consent session's access_token data in the "ext" field of introspection
|
// Hydra injects the consent session's access_token data there.
|
||||||
var sessionClaims claims.Claims
|
var sessionClaims claims.Claims
|
||||||
|
sessionClaims.Session.AccessToken = make(map[string]interface{})
|
||||||
|
sessionClaims.Session.IDToken = make(map[string]interface{})
|
||||||
if result.Extra != nil {
|
if result.Extra != nil {
|
||||||
sessionClaims.Session.AccessToken = make(map[string]interface{})
|
|
||||||
sessionClaims.Session.IDToken = make(map[string]interface{})
|
|
||||||
for k, v := range result.Extra {
|
for k, v := range result.Extra {
|
||||||
sessionClaims.Session.AccessToken[k] = v
|
sessionClaims.Session.AccessToken[k] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also try to get id_token claims from the token if it's a JWT
|
// For SELF peer requests skip the signature check (internal traffic).
|
||||||
// For now, use the introspected extra claims and the peer signature verification
|
|
||||||
if sessionClaims.Session.IDToken == nil {
|
|
||||||
sessionClaims.Session.IDToken = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get self peer for signature verification
|
|
||||||
pp := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), "", "", []string{}, nil).Search(nil, fmt.Sprintf("%v", peer.SELF.EnumIndex()), false)
|
pp := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), "", "", []string{}, nil).Search(nil, fmt.Sprintf("%v", peer.SELF.EnumIndex()), false)
|
||||||
if len(pp.Data) > 0 {
|
if len(pp.Data) > 0 {
|
||||||
p := pp.Data[0].(*peer.Peer)
|
p := pp.Data[0].(*peer.Peer)
|
||||||
// Re-sign for local verification if this is our own peer
|
if p.PublicKey == publicKey {
|
||||||
if !external && p.PublicKey == publicKey {
|
|
||||||
sessionClaims.Session.IDToken["signature"] = ""
|
sessionClaims.Session.IDToken["signature"] = ""
|
||||||
// For internal requests, skip signature check by using the claims decoder directly
|
|
||||||
ok, err := claims.GetClaims().DecodeClaimsInToken(host, method, forward, sessionClaims, publicKey, external)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error().Msg("Failed to decode claims: " + err.Error())
|
|
||||||
}
|
|
||||||
return ok
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check permissions via Keto.
|
||||||
|
// A valid token with insufficient permissions → 403 (authenticated, not authorized).
|
||||||
ok, err := claims.GetClaims().DecodeClaimsInToken(host, method, forward, sessionClaims, publicKey, external)
|
ok, err := claims.GetClaims().DecodeClaimsInToken(host, method, forward, sessionClaims, publicKey, external)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Msg("Failed to decode claims: " + err.Error())
|
logger.Error().Msg("Failed to decode claims in forward auth: " + err.Error())
|
||||||
|
return http.StatusForbidden
|
||||||
}
|
}
|
||||||
return ok
|
if !ok {
|
||||||
|
return http.StatusForbidden
|
||||||
|
}
|
||||||
|
return http.StatusOK
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractBearerToken extracts the token from a "Bearer xxx" Authorization header value
|
// extractBearerToken extracts the token from a "Bearer xxx" Authorization header value
|
||||||
|
|||||||
@@ -370,17 +370,20 @@ func (k KetoConnector) createRelationShip(object string, relation string, subjec
|
|||||||
log.Error().Msgf("createRelationShip unmarshal error: %s, err=%v", string(b), err)
|
log.Error().Msgf("createRelationShip unmarshal error: %s, err=%v", string(b), err)
|
||||||
return nil, 500, err
|
return nil, 500, err
|
||||||
}
|
}
|
||||||
perm := &Permission{
|
perm := &Permission{}
|
||||||
Object: data["object"].(string),
|
if data != nil {
|
||||||
Relation: data["relation"].(string),
|
perm = &Permission{
|
||||||
Subject: data["subject_id"].(string),
|
Object: data["object"].(string),
|
||||||
}
|
Relation: data["relation"].(string),
|
||||||
if data["subject_set"] != nil {
|
Subject: data["subject_id"].(string),
|
||||||
sub := data["subject_set"].(map[string]interface{})
|
}
|
||||||
perm.SubPermission = &Permission{
|
if data["subject_set"] != nil {
|
||||||
Object: sub["object"].(string),
|
sub := data["subject_set"].(map[string]interface{})
|
||||||
Relation: sub["relation"].(string),
|
perm.SubPermission = &Permission{
|
||||||
Subject: sub["subject_id"].(string),
|
Object: sub["object"].(string),
|
||||||
|
Relation: sub["relation"].(string),
|
||||||
|
Subject: sub["subject_id"].(string),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return perm, 200, nil
|
return perm, 200, nil
|
||||||
|
|||||||
25
main.go
25
main.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"oc-auth/infrastructure"
|
"oc-auth/infrastructure"
|
||||||
auth_connectors "oc-auth/infrastructure/auth_connector"
|
auth_connectors "oc-auth/infrastructure/auth_connector"
|
||||||
_ "oc-auth/routers"
|
_ "oc-auth/routers"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
oclib "cloud.o-forge.io/core/oc-lib"
|
oclib "cloud.o-forge.io/core/oc-lib"
|
||||||
@@ -40,9 +41,15 @@ func main() {
|
|||||||
conf.GetConfig().PermissionConnectorReadHost = o.GetStringDefault("PERMISSION_CONNECTOR_READ_HOST", "keto")
|
conf.GetConfig().PermissionConnectorReadHost = o.GetStringDefault("PERMISSION_CONNECTOR_READ_HOST", "keto")
|
||||||
conf.GetConfig().PermissionConnectorPort = o.GetStringDefault("PERMISSION_CONNECTOR_PORT", "4466")
|
conf.GetConfig().PermissionConnectorPort = o.GetStringDefault("PERMISSION_CONNECTOR_PORT", "4466")
|
||||||
conf.GetConfig().PermissionConnectorAdminPort = o.GetStringDefault("PERMISSION_CONNECTOR_ADMIN_PORT", "4467")
|
conf.GetConfig().PermissionConnectorAdminPort = o.GetStringDefault("PERMISSION_CONNECTOR_ADMIN_PORT", "4467")
|
||||||
|
|
||||||
|
conf.GetConfig().Origin = o.GetStringDefault("ADMIN_ORIGIN", "http://localhost:8000")
|
||||||
|
conf.GetConfig().AdminOrigin = o.GetStringDefault("ADMIN_ORIGIN", "http://localhost:8001")
|
||||||
|
|
||||||
|
conf.GetConfig().OAuthRedirectURI = o.GetStringDefault("OAUTH_REDIRECT_URI", "http://localhost:8000/l")
|
||||||
|
conf.GetConfig().OAdminAuthRedirectURI = o.GetStringDefault("ADMIN_OAUTH_REDIRECT_URI", "http://localhost:8000/l")
|
||||||
conf.GetConfig().Local = o.GetBoolDefault("LOCAL", true)
|
conf.GetConfig().Local = o.GetBoolDefault("LOCAL", true)
|
||||||
|
|
||||||
// config LDAP
|
// config LDAPauth
|
||||||
conf.GetConfig().SourceMode = o.GetStringDefault("SOURCE_MODE", "ldap")
|
conf.GetConfig().SourceMode = o.GetStringDefault("SOURCE_MODE", "ldap")
|
||||||
conf.GetConfig().LDAPEndpoints = o.GetStringDefault("LDAP_ENDPOINTS", "ldap:389")
|
conf.GetConfig().LDAPEndpoints = o.GetStringDefault("LDAP_ENDPOINTS", "ldap:389")
|
||||||
conf.GetConfig().LDAPBindDN = o.GetStringDefault("LDAP_BINDDN", "cn=admin,dc=example,dc=com")
|
conf.GetConfig().LDAPBindDN = o.GetStringDefault("LDAP_BINDDN", "cn=admin,dc=example,dc=com")
|
||||||
@@ -100,17 +107,23 @@ func discovery() {
|
|||||||
logger.Info().Msg("Starting permission discovery")
|
logger.Info().Msg("Starting permission discovery")
|
||||||
_, _, err := conn.CreateRole(conf.GetConfig().AdminRole)
|
_, _, err := conn.CreateRole(conf.GetConfig().AdminRole)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error().Msg("Failed to create admin role, retrying in 10s: " + err.Error())
|
if !strings.Contains(err.Error(), "already exist") {
|
||||||
time.Sleep(10 * time.Second)
|
logger.Error().Msg("Failed to create admin role, retrying in 10s: " + err.Error())
|
||||||
continue
|
time.Sleep(10 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, _, err := conn.BindRole(conf.GetConfig().AdminRole, "admin"); err != nil {
|
||||||
|
logger.Error().Msg("Failed to admin bind role: " + err.Error())
|
||||||
}
|
}
|
||||||
conn.BindRole(conf.GetConfig().AdminRole, "admin")
|
|
||||||
addPermissions := func(m tools.NATSResponse) {
|
addPermissions := func(m tools.NATSResponse) {
|
||||||
var resp map[string][]interface{}
|
var resp map[string][]interface{}
|
||||||
json.Unmarshal(m.Payload, &resp)
|
json.Unmarshal(m.Payload, &resp)
|
||||||
for k, v := range resp {
|
for k, v := range resp {
|
||||||
for _, p := range v {
|
for _, p := range v {
|
||||||
conn.CreatePermission(k, p.(string), true)
|
if _, _, err := conn.CreatePermission(k, p.(string), true); err != nil {
|
||||||
|
logger.Error().Msg("Failed to admin create permission: " + err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,13 @@
|
|||||||
"description": "The consent challenge from Hydra",
|
"description": "The consent challenge from Hydra",
|
||||||
"required": true,
|
"required": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "redirect",
|
||||||
|
"description": "explicit redirect by passed",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -52,7 +59,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"oc-auth/controllersOAuthController"
|
"oc-auth/controllersOAuthController"
|
||||||
],
|
],
|
||||||
"description": "Forward auth for Traefik — validates JWT via Hydra introspection\n\u003cbr\u003e",
|
"description": "Forward auth for Traefik — validates JWT via Hydra introspection.\n\u003cbr\u003e",
|
||||||
"operationId": "OAuthController.AuthForward",
|
"operationId": "OAuthController.AuthForward",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -282,6 +289,13 @@
|
|||||||
"description": "The login challenge from Hydra",
|
"description": "The login challenge from Hydra",
|
||||||
"required": true,
|
"required": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "redirect",
|
||||||
|
"description": "explicit redirect by passed",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -304,8 +318,15 @@
|
|||||||
"oc-auth/controllersOAuthController"
|
"oc-auth/controllersOAuthController"
|
||||||
],
|
],
|
||||||
"description": "Authenticate user via LDAP and accept Hydra login challenge\n\u003cbr\u003e",
|
"description": "Authenticate user via LDAP and accept Hydra login challenge\n\u003cbr\u003e",
|
||||||
"operationId": "OAuthController.PostLogin",
|
"operationId": "OAuthController.Login",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "redirect",
|
||||||
|
"description": "explicit redirect by passed",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"in": "body",
|
"in": "body",
|
||||||
"name": "body",
|
"name": "body",
|
||||||
@@ -346,6 +367,13 @@
|
|||||||
"description": "The logout challenge from Hydra",
|
"description": "The logout challenge from Hydra",
|
||||||
"required": true,
|
"required": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"in": "query",
|
||||||
|
"name": "redirect",
|
||||||
|
"description": "explicit redirect by passed",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -798,14 +826,29 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"2111.0xc0004ce750.false": {
|
"2432.0xc0004a0630.false": {
|
||||||
"title": "false",
|
"title": "false",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
"3850.0xc0004ce930.false": {
|
"4171.0xc0004a0810.false": {
|
||||||
"title": "false",
|
"title": "false",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"auth_connectors.LoginRequest": {
|
||||||
|
"title": "LoginRequest",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"login_challenge": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"auth_connectors.IntrospectResult": {
|
"auth_connectors.IntrospectResult": {
|
||||||
"title": "IntrospectResult",
|
"title": "IntrospectResult",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -821,7 +864,7 @@
|
|||||||
"format": "int64"
|
"format": "int64"
|
||||||
},
|
},
|
||||||
"ext": {
|
"ext": {
|
||||||
"$ref": "#/definitions/3850.0xc0004ce930.false"
|
"$ref": "#/definitions/4171.0xc0004a0810.false"
|
||||||
},
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -842,7 +885,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"client": {
|
"client": {
|
||||||
"$ref": "#/definitions/2111.0xc0004ce750.false"
|
"$ref": "#/definitions/2432.0xc0004a0630.false"
|
||||||
},
|
},
|
||||||
"request_url": {
|
"request_url": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -858,21 +901,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth_connectors.LoginRequest": {
|
|
||||||
"title": "LoginRequest",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"login_challenge": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"password": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"username": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"auth_connectors.Redirect": {
|
"auth_connectors.Redirect": {
|
||||||
"title": "Redirect",
|
"title": "Redirect",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ paths:
|
|||||||
description: The consent challenge from Hydra
|
description: The consent challenge from Hydra
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: redirect
|
||||||
|
description: explicit redirect by passed
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: ""
|
description: ""
|
||||||
@@ -40,7 +45,7 @@ paths:
|
|||||||
tags:
|
tags:
|
||||||
- oc-auth/controllersOAuthController
|
- oc-auth/controllersOAuthController
|
||||||
description: |-
|
description: |-
|
||||||
Forward auth for Traefik — validates JWT via Hydra introspection
|
Forward auth for Traefik — validates JWT via Hydra introspection.
|
||||||
<br>
|
<br>
|
||||||
operationId: OAuthController.AuthForward
|
operationId: OAuthController.AuthForward
|
||||||
parameters:
|
parameters:
|
||||||
@@ -214,6 +219,11 @@ paths:
|
|||||||
description: The login challenge from Hydra
|
description: The login challenge from Hydra
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: redirect
|
||||||
|
description: explicit redirect by passed
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: ""
|
description: ""
|
||||||
@@ -229,8 +239,13 @@ paths:
|
|||||||
description: |-
|
description: |-
|
||||||
Authenticate user via LDAP and accept Hydra login challenge
|
Authenticate user via LDAP and accept Hydra login challenge
|
||||||
<br>
|
<br>
|
||||||
operationId: OAuthController.PostLogin
|
operationId: OAuthController.Login
|
||||||
parameters:
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: redirect
|
||||||
|
description: explicit redirect by passed
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
- in: body
|
- in: body
|
||||||
name: body
|
name: body
|
||||||
description: Login credentials and challenge
|
description: Login credentials and challenge
|
||||||
@@ -260,6 +275,11 @@ paths:
|
|||||||
description: The logout challenge from Hydra
|
description: The logout challenge from Hydra
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
- in: query
|
||||||
|
name: redirect
|
||||||
|
description: explicit redirect by passed
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: ""
|
description: ""
|
||||||
@@ -593,12 +613,22 @@ paths:
|
|||||||
"200":
|
"200":
|
||||||
description: ""
|
description: ""
|
||||||
definitions:
|
definitions:
|
||||||
2111.0xc0004ce750.false:
|
2432.0xc0004a0630.false:
|
||||||
title: "false"
|
title: "false"
|
||||||
type: object
|
type: object
|
||||||
3850.0xc0004ce930.false:
|
4171.0xc0004a0810.false:
|
||||||
title: "false"
|
title: "false"
|
||||||
type: object
|
type: object
|
||||||
|
auth_connectors.LoginRequest:
|
||||||
|
title: LoginRequest
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
type: string
|
||||||
|
login_challenge:
|
||||||
|
type: string
|
||||||
auth_connectors.IntrospectResult:
|
auth_connectors.IntrospectResult:
|
||||||
title: IntrospectResult
|
title: IntrospectResult
|
||||||
type: object
|
type: object
|
||||||
@@ -611,7 +641,7 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
format: int64
|
format: int64
|
||||||
ext:
|
ext:
|
||||||
$ref: '#/definitions/3850.0xc0004ce930.false'
|
$ref: '#/definitions/4171.0xc0004a0810.false'
|
||||||
scope:
|
scope:
|
||||||
type: string
|
type: string
|
||||||
sub:
|
sub:
|
||||||
@@ -625,7 +655,7 @@ definitions:
|
|||||||
challenge:
|
challenge:
|
||||||
type: string
|
type: string
|
||||||
client:
|
client:
|
||||||
$ref: '#/definitions/2111.0xc0004ce750.false'
|
$ref: '#/definitions/2432.0xc0004a0630.false'
|
||||||
request_url:
|
request_url:
|
||||||
type: string
|
type: string
|
||||||
session_id:
|
session_id:
|
||||||
@@ -634,16 +664,6 @@ definitions:
|
|||||||
type: boolean
|
type: boolean
|
||||||
subject:
|
subject:
|
||||||
type: string
|
type: string
|
||||||
auth_connectors.LoginRequest:
|
|
||||||
title: LoginRequest
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
login_challenge:
|
|
||||||
type: string
|
|
||||||
password:
|
|
||||||
type: string
|
|
||||||
username:
|
|
||||||
type: string
|
|
||||||
auth_connectors.Redirect:
|
auth_connectors.Redirect:
|
||||||
title: Redirect
|
title: Redirect
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
Reference in New Issue
Block a user