1 Commits

Author SHA1 Message Date
pb
28e6f1e043 trying to apply oclib changes 2025-01-03 16:55:12 +01:00
35 changed files with 850 additions and 2127 deletions

3
.gitattributes vendored
View File

@@ -1,3 +0,0 @@
# Force Go as the main language
*.go linguist-detectable=true
* linguist-language=Go

2
.gitignore vendored
View File

@@ -20,4 +20,4 @@
# Go workspace file # Go workspace file
go.work go.work
env.env

View File

@@ -10,6 +10,11 @@ RUN go mod download
FROM golang:alpine AS builder FROM golang:alpine AS builder
ARG HOSTNAME=http://localhost
ARG NAME=auth
RUN apk add git
RUN go install github.com/beego/bee/v2@latest RUN go install github.com/beego/bee/v2@latest
WORKDIR /oc-auth WORKDIR /oc-auth

View File

@@ -6,14 +6,6 @@ build: clean
run: run:
bee run -gendoc=true -downdoc=true bee run -gendoc=true -downdoc=true
purge:
lsof -t -i:8094 | xargs kill | true
run-dev:
bee generate routers && bee run -gendoc=true -downdoc=true -runmode=prod
dev: purge run-dev
debug: debug:
bee run -downdebug -gendebug bee run -downdebug -gendebug
@@ -21,22 +13,15 @@ clean:
rm -rf oc-auth oc-auth.tar.gz rm -rf oc-auth oc-auth.tar.gz
docker: docker:
DOCKER_BUILDKIT=1 docker build -t oc-auth -f Dockerfile . --build-arg=HOST=$(HOST) DOCKER_BUILDKIT=1 docker build -t oc/oc-auth:0.0.1 -f Dockerfile .
docker tag oc-auth opencloudregistry/oc-auth:latest docker tag oc/oc-auth:0.0.1 oc/oc-auth:latest
publish-kind: publish-kind:
kind load docker-image opencloudregistry/oc-auth:latest --name $(CLUSTER_NAME) | true kind load docker-image oc/oc-auth:0.0.1 --name opencloud
publish-registry: publish-registry:
docker push opencloudregistry/oc-auth:latest @echo "TODO"
docker-deploy: all: docker publish-kind publish-registry
docker compose up -d
run-docker: docker publish-kind publish-registry docker-deploy .PHONY: build run clean docker publish-kind publish-registry
all: docker publish-kind
ci: docker publish-registry
.PHONY: build run clean docker publish-kind publish-registry

View File

@@ -7,76 +7,7 @@ To build :
bee generate routers bee generate routers
bee run -gendoc=true -downdoc=true bee run -gendoc=true -downdoc=true
OR
make dev
If default Swagger page is displayed instead of tyour api, change url in swagger/index.html file to : If default Swagger page is displayed instead of tyour api, change url in swagger/index.html file to :
url: "swagger.json" url: "swagger.json"
┌─────────┐ ┌──────────┐ ┌────────────┐ ┌────────┐
│ Browser │ │ UI │ │ Hydra │ │ API │
└────┬────┘ └────┬─────┘ └────┬───────┘ └───┬────┘
│ │ │ │
│ 1. Click "Login" │ │ │
│─────────────────► │ │ │
│ │ │ │
│ 2. Redirect auth │──────────────────► │ │
│ │ /oauth2/auth │ │
│ │ │ │
│ │ ◄──────────────────│ │
│ │ login challenge │ │
│ │ │ │
│ │ 3. Login UI │ │
│ │ (credentials) │ │
│ │ │ │
│ │──────────────────► │ │
│ │ accept login │ │
│ │ │ │
│ │ ◄──────────────────│ │
│ │ consent challenge │ │
│ │ │ │
│ │ 4. CALL API │ │
│ │──────────────────────────────────────► │
│ │ fetch peer / roles │
│ │ │ │
│ │ ◄──────────────────────────────────────│
│ │ peer, permissions │
│ │ │ │
│ │ 5. Accept consent │ │
│ │──────────────────► │ │
│ │ + custom claims │ │
│ │ │ │
│ │ ◄──────────────────│ │
│ │ redirect w/ code │ │
│ │ │ │
│ 6. Exchange code │──────────────────► │ │
│ for token │ /oauth2/token │ │
│ │ │ │
│ ◄─────────────────│ │ │
│ 7. JWT access_token │ │
│ (signed + enriched) │
│ │
│ 8. API call with Bearer token │
│───────────────────────────────────────────────────────────►│
│ │
│ ◄──────────────────────────────────────────────────────────│
Browser
Hydra /oauth2/auth
Redirect → /login?login_challenge=abc123
Frontend Login Page
POST username/password/login_challenge
TON backend
Hydra Admin API (accept login)
Hydra retourne redirect_to
Frontend redirige

View File

@@ -1,7 +1,9 @@
{ {
"port": 8080,
"MONGO_URL":"mongodb://localhost:27017/", "MONGO_URL":"mongodb://localhost:27017/",
"MONGO_DATABASE":"DC_myDC", "MONGO_DATABASE":"DC_myDC",
"NATS_URL": "nats://localhost:4222", "natsurl":"http://localhost:4080",
"LDAP_ENDPOINTS": "localhost:390", "login":"admin",
"port": 8094 "password":"admin",
"oidcserver":"http://localhost:8080"
} }

View File

@@ -1,5 +1,5 @@
appname = oc-auth appname = oc-auth
httpport = 8094 httpport = 8080
runmode = dev runmode = dev
autorender = false autorender = false
copyrequestbody = true copyrequestbody = true

View File

@@ -3,7 +3,6 @@ package conf
import "sync" import "sync"
type Config struct { type Config struct {
SourceMode string
AdminRole string AdminRole string
PublicKeyPath string PublicKeyPath string
PrivateKeyPath string PrivateKeyPath string
@@ -12,34 +11,18 @@ type Config struct {
LDAPBindDN string LDAPBindDN string
LDAPBindPW string LDAPBindPW string
LDAPBaseDN string LDAPBaseDN string
LDAPUserBaseDN string
LDAPRoleBaseDN string LDAPRoleBaseDN string
ClientSecret string ClientSecret string
OAuth2ClientSecretName string
OAuth2ClientSecretNamespace string
Auth string Auth string
AuthConnectPublicHost string
AuthConnectorHost string AuthConnectorHost string
AuthConnectorPort int AuthConnectorPort int
AuthConnectorAdminPort string AuthConnectorAdminPort int
PermissionConnectorWriteHost string PermissionConnectorHost string
PermissionConnectorReadHost string PermissionConnectorPort int
PermissionConnectorPort string PermissionConnectorAdminPort int
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
} }
var instance *Config var instance *Config

View File

@@ -19,8 +19,7 @@ type GroupController struct {
func (o *GroupController) Post() { func (o *GroupController) Post() {
// store and return Id or post with UUID // store and return Id or post with UUID
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
clientID := ExtractClient(*o.Ctx.Request) group, code, err := infrastructure.GetPermissionConnector().CreateGroup(id)
group, code, err := infrastructure.GetPermissionConnector(clientID).CreateGroup(id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -45,8 +44,7 @@ func (o *GroupController) Post() {
// @router /user/:id [get] // @router /user/:id [get]
func (o *GroupController) GetByUser() { func (o *GroupController) GetByUser() {
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
clientID := ExtractClient(*o.Ctx.Request) group, err := infrastructure.GetPermissionConnector().GetGroupByUser(id)
group, err := infrastructure.GetPermissionConnector(clientID).GetGroupByUser(id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -69,8 +67,7 @@ func (o *GroupController) GetByUser() {
// @Success 200 {group} string // @Success 200 {group} string
// @router / [get] // @router / [get]
func (o *GroupController) GetAll() { func (o *GroupController) GetAll() {
clientID := ExtractClient(*o.Ctx.Request) group, err := infrastructure.GetPermissionConnector().GetGroup("")
group, err := infrastructure.GetPermissionConnector(clientID).GetGroup("")
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -95,8 +92,7 @@ func (o *GroupController) GetAll() {
// @router /:id [get] // @router /:id [get]
func (o *GroupController) Get() { func (o *GroupController) Get() {
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
clientID := ExtractClient(*o.Ctx.Request) group, err := infrastructure.GetPermissionConnector().GetGroup(id)
group, err := infrastructure.GetPermissionConnector(clientID).GetGroup(id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -121,8 +117,7 @@ func (o *GroupController) Get() {
// @router /:id [delete] // @router /:id [delete]
func (o *GroupController) Delete() { func (o *GroupController) Delete() {
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
clientID := ExtractClient(*o.Ctx.Request) group, code, err := infrastructure.GetPermissionConnector().DeleteGroup(id)
group, code, err := infrastructure.GetPermissionConnector(clientID).DeleteGroup(id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -145,8 +140,7 @@ func (o *GroupController) Delete() {
// @Success 200 {string} delete success! // @Success 200 {string} delete success!
// @router /clear [delete] // @router /clear [delete]
func (o *GroupController) Clear() { func (o *GroupController) Clear() {
clientID := ExtractClient(*o.Ctx.Request) group, code, err := infrastructure.GetPermissionConnector().DeleteGroup("")
group, code, err := infrastructure.GetPermissionConnector(clientID).DeleteGroup("")
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -173,8 +167,7 @@ func (o *GroupController) Clear() {
func (o *GroupController) Bind() { func (o *GroupController) Bind() {
user_id := o.Ctx.Input.Param(":user_id") user_id := o.Ctx.Input.Param(":user_id")
group_id := o.Ctx.Input.Param(":group_id") group_id := o.Ctx.Input.Param(":group_id")
clientID := ExtractClient(*o.Ctx.Request) group, code, err := infrastructure.GetPermissionConnector().BindGroup(user_id, group_id)
group, code, err := infrastructure.GetPermissionConnector(clientID).BindGroup(user_id, group_id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -194,15 +187,14 @@ func (o *GroupController) Bind() {
// @Title UnBind // @Title UnBind
// @Description unbind the group to user // @Description unbind the group to user
// @Param user_id path string true "The group_id you want to unbind" // @Param group_id path string true "The group_id you want to unbind"
// @Param group_id path string true "The user_id you want to unbind" // @Param group_id path string true "The user_id you want to unbind"
// @Success 200 {string} bind success! // @Success 200 {string} bind success!
// @router /:user_id/:group_id [delete] // @router /:user_id/:group_id [delete]
func (o *GroupController) UnBind() { func (o *GroupController) UnBind() {
user_id := o.Ctx.Input.Param(":user_id") user_id := o.Ctx.Input.Param(":user_id")
group_id := o.Ctx.Input.Param(":group_id") group_id := o.Ctx.Input.Param(":group_id")
clientID := ExtractClient(*o.Ctx.Request) group, code, err := infrastructure.GetPermissionConnector().UnBindGroup(user_id, group_id)
group, code, err := infrastructure.GetPermissionConnector(clientID).UnBindGroup(user_id, group_id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,

View File

@@ -1,507 +1,167 @@
package controllers package controllers
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"oc-auth/conf"
"oc-auth/infrastructure" "oc-auth/infrastructure"
auth_connectors "oc-auth/infrastructure/auth_connector" auth_connectors "oc-auth/infrastructure/auth_connector"
"regexp" "regexp"
"strconv"
"strings" "strings"
"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"
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"
) )
// OAuthController handles OAuth2 login/consent provider endpoints // Operations about auth
type OAuthController struct { type OAuthController struct {
beego.Controller beego.Controller
} }
// @Title GetLogin
// @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 redirect query string true "explicit redirect by passed"
// @Success 200 {object} auth_connectors.LoginChallenge
// @Failure 400 missing login_challenge
// @Failure 500 internal error
// @router /login [get]
func (o *OAuthController) GetLogin() {
logger := oclib.GetLogger()
challenge := o.Ctx.Input.Query("login_challenge")
if challenge == "" {
o.Ctx.ResponseWriter.WriteHeader(400)
o.Data["json"] = map[string]string{"error": "missing login_challenge parameter"}
o.ServeJSON()
return
}
if conf.GetConfig().Local {
// In local mode, return a mock challenge for dev
o.Data["json"] = &auth_connectors.LoginChallenge{
Skip: false,
Challenge: challenge,
}
o.ServeJSON()
return
}
loginChallenge, err := infrastructure.GetAuthConnector().GetLoginChallenge(challenge)
if err != nil {
logger.Error().Msg("Failed to get login challenge: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
o.ServeJSON()
return
}
// If skip is true, the user already has an active session — auto-accept
if loginChallenge.Skip {
redirect, err := infrastructure.GetAuthConnector().AcceptLogin(challenge, loginChallenge.Subject)
if err != nil {
logger.Error().Msg("Failed to auto-accept login: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
o.ServeJSON()
return
}
o.Data["json"] = redirect
o.ServeJSON()
return
return
}
// Return challenge info so frontend can render login form
o.Data["json"] = loginChallenge
o.ServeJSON()
}
// @Title PostLogin
// @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"
// @Success 200 {object} auth_connectors.Redirect
// @Failure 401 invalid credentials
// @Failure 500 internal error
// @router /login [post]
func (o *OAuthController) Login() {
logger := oclib.GetLogger()
red := o.Ctx.Input.Query("redirect")
var req auth_connectors.LoginRequest
if err := json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &req); err != nil {
o.Ctx.ResponseWriter.WriteHeader(400)
o.Data["json"] = map[string]string{"error": "invalid request body"}
o.ServeJSON()
return
}
if req.Username == "" || req.Password == "" {
o.Ctx.ResponseWriter.WriteHeader(400)
o.Data["json"] = map[string]string{"error": "username and password are required"}
o.ServeJSON()
return
}
// Authenticate via LDAP
ldap := auth_connectors.New()
found, err := ldap.Authenticate(o.Ctx.Request.Context(), req.Username, req.Password)
if err != nil || !found {
logger.Error().Msg("LDAP authentication failed for user: " + req.Username)
o.Ctx.ResponseWriter.WriteHeader(401)
o.Data["json"] = map[string]string{"error": "invalid credentials"}
o.ServeJSON()
return
}
if conf.GetConfig().Local {
// In local mode, return a mock token for dev
t := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), "", "", []string{}, nil).Search(
nil, fmt.Sprintf("%v", model.SELF.EnumIndex()), false)
if t.Err == "" && len(t.Data) > 0 {
p := t.Data[0].(*model.Peer)
c := infrastructure.GetClaims().BuildConsentSession("local", req.Username, p)
now := time.Now().UTC()
now = now.Add(3600 * time.Second)
c.Session.AccessToken["exp"] = now.Unix()
b, _ := json.Marshal(c)
token := &auth_connectors.Token{
Active: true,
TokenType: "Bearer",
ExpiresIn: 3600,
AccessToken: "localtoken." + base64.StdEncoding.EncodeToString(b),
}
o.Data["json"] = token
} else {
o.Ctx.ResponseWriter.WriteHeader(401)
o.Data["json"] = map[string]string{"error": "peer not found"}
}
o.ServeJSON()
return
}
if req.LoginChallenge == "" {
o.Ctx.ResponseWriter.WriteHeader(400)
o.Data["json"] = map[string]string{"error": "login_challenge is required in non-local mode"}
o.ServeJSON()
return
}
// Accept the login challenge with Hydra
redirect, err := infrastructure.GetAuthConnector().AcceptLogin(req.LoginChallenge, req.Username)
if err != nil {
logger.Error().Msg("Failed to accept login: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
o.ServeJSON()
return
}
// Return redirect_to so the frontend follows the OAuth2 flow
if red == "false" {
o.Data["json"] = redirect
o.ServeJSON()
return
}
o.Redirect(redirect.RedirectTo, 303)
}
// @Title Consent
// @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 redirect query string true "explicit redirect by passed"
// @Success 200 {object} auth_connectors.Redirect
// @Failure 400 missing consent_challenge
// @Failure 500 internal error
// @router /consent [get]
func (o *OAuthController) Consent() {
logger := oclib.GetLogger()
challenge := o.Ctx.Input.Query("consent_challenge")
if challenge == "" {
o.Ctx.ResponseWriter.WriteHeader(400)
o.Data["json"] = map[string]string{"error": "missing consent_challenge parameter"}
o.ServeJSON()
return
}
// Get consent challenge details from Hydra
consentChallenge, err := infrastructure.GetAuthConnector().GetConsentChallenge(challenge)
if err != nil {
logger.Error().Msg("Failed to get consent challenge: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
o.ServeJSON()
return
}
// Get self peer for signing
pp := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil).Search(
&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 != "" {
logger.Error().Msg("Self peer not found")
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": "self peer not found"}
o.ServeJSON()
return
}
p := pp.Data[0].(*peer.Peer)
// Extract client_id from consent challenge
clientID := ""
if consentChallenge.Client != nil {
if cid, ok := consentChallenge.Client["client_id"].(string); ok {
clientID = cid
}
}
// Build consent session with user permissions and claims
session := infrastructure.GetClaims().BuildConsentSession(clientID, consentChallenge.Subject, p)
// Accept the consent challenge — grant all requested scopes
redirect, err := infrastructure.GetAuthConnector().AcceptConsent(challenge, consentChallenge.RequestedScope, session)
if err != nil {
logger.Error().Msg("Failed to accept consent: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
o.ServeJSON()
return
}
// Return redirect_to (callback URL with authorization code)
o.Data["json"] = redirect
o.ServeJSON()
}
// @Title GetLogout
// @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 redirect query string true "explicit redirect by passed"
// @Success 200 {object} auth_connectors.Redirect
// @Failure 400 missing logout_challenge
// @Failure 500 internal error
// @router /logout [get]
func (o *OAuthController) GetLogout() {
logger := oclib.GetLogger()
red := o.Ctx.Input.Query("redirect")
challenge := o.Ctx.Input.Query("logout_challenge")
if challenge == "" {
o.Ctx.ResponseWriter.WriteHeader(400)
o.Data["json"] = map[string]string{"error": "missing logout_challenge parameter"}
o.ServeJSON()
return
}
if conf.GetConfig().Local {
o.Data["json"] = &auth_connectors.Redirect{RedirectTo: ""}
o.ServeJSON()
return
}
_, err := infrastructure.GetAuthConnector().GetLogoutChallenge(challenge)
if err != nil {
logger.Error().Msg("Failed to get logout challenge: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
o.ServeJSON()
return
}
redirect, err := infrastructure.GetAuthConnector().AcceptLogout(challenge)
if err != nil {
logger.Error().Msg("Failed to accept logout challenge: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
o.ServeJSON()
return
}
if red == "false" {
o.Data["json"] = redirect
o.ServeJSON()
return
}
o.Redirect(redirect.RedirectTo, 303)
}
// @Title Logout // @Title Logout
// @Description Revoke an OAuth2 token // @Description unauthenticate user
// @Param Authorization header string false "Bearer token" // @Param Authorization header string false "auth token"
// @Param client_id query string true "The client_id" // @Success 200 {string}
// @router /ldap/logout [delete]
// @Success 200 {object} auth_connectors.Token func (o *OAuthController) LogOutLDAP() {
// @router /logout [delete] // authorize user
func (o *OAuthController) LogOut() { reqToken := o.Ctx.Request.Header.Get("Authorization")
clientID := o.Ctx.Input.Query("client_id") splitToken := strings.Split(reqToken, "Bearer ")
reqToken := extractBearerToken(o.Ctx.Request) if len(splitToken) < 2 {
reqToken = ""
if conf.GetConfig().Local {
o.Data["json"] = map[string]string{"status": "logged out"}
o.ServeJSON()
return
}
err := infrastructure.GetAuthConnector().RevokeToken(reqToken, clientID)
if err != nil {
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
} else { } else {
o.Data["json"] = &auth_connectors.Token{ reqToken = splitToken[1]
AccessToken: reqToken,
Active: false,
}
} }
o.ServeJSON() var res auth_connectors.Token
} json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &res)
// @Title Refresh token, err := infrastructure.GetAuthConnector().Logout(reqToken)
// @Description Exchange a refresh_token for a new token set if err != nil || token == nil {
// @Param body body object true "refresh_token and client_id" o.Data["json"] = err
// @Success 200 {object} auth_connectors.TokenResponse
// @Failure 401 invalid refresh token
// @router /refresh [post]
func (o *OAuthController) Refresh() {
logger := oclib.GetLogger()
var body struct {
RefreshToken string `json:"refresh_token"`
ClientID string `json:"client_id"`
}
json.Unmarshal(o.Ctx.Input.CopyBody(100000), &body)
if conf.GetConfig().Local {
o.Data["json"] = map[string]string{"error": "refresh not supported in local mode"}
o.Ctx.ResponseWriter.WriteHeader(400)
o.ServeJSON()
return
}
if body.RefreshToken == "" {
o.Ctx.ResponseWriter.WriteHeader(400)
o.Data["json"] = map[string]string{"error": "refresh_token is required"}
o.ServeJSON()
return
}
token, err := infrastructure.GetAuthConnector().RefreshToken(body.RefreshToken, body.ClientID)
if err != nil {
logger.Error().Msg("Failed to refresh token: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(401)
o.Data["json"] = map[string]string{"error": err.Error()}
} else { } else {
o.Data["json"] = token o.Data["json"] = token
} }
o.ServeJSON() o.ServeJSON()
} }
// @Title Introspect // @Title Login
// @Description Introspect a token — respects Hydra's response // @Description authenticate user
// @Param Authorization header string false "Bearer token" // @Param body body models.workflow true "The workflow content"
// @Success 200 {object} auth_connectors.IntrospectResult // @Success 200 {string}
// @router /introspect [get] // @router /ldap/login [post]
func (o *OAuthController) Introspect() { func (o *OAuthController) LoginLDAP() {
reqToken := extractBearerToken(o.Ctx.Request) // authorize user
if reqToken == "" { var res auth_connectors.Token
json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &res)
ldap := auth_connectors.New()
found, err := ldap.Authenticate(o.Ctx.Request.Context(), res.Username, res.Password)
if err != nil || !found {
o.Data["json"] = err
o.Ctx.ResponseWriter.WriteHeader(401) o.Ctx.ResponseWriter.WriteHeader(401)
o.Data["json"] = map[string]string{"error": "missing bearer token"}
o.ServeJSON() o.ServeJSON()
return return
} }
token, err := infrastructure.GetAuthConnector().Login(res.Username,
if conf.GetConfig().Local { &http.Cookie{ // open a session
o.Data["json"] = &auth_connectors.IntrospectResult{Active: true} Name: "csrf_token",
o.ServeJSON() Value: o.XSRFToken(),
return })
} if err != nil || token == nil {
o.Data["json"] = err
result, err := infrastructure.GetAuthConnector().Introspect(reqToken)
if err != nil {
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
} else if !result.Active {
o.Ctx.ResponseWriter.WriteHeader(401) o.Ctx.ResponseWriter.WriteHeader(401)
o.Data["json"] = result
} else { } else {
o.Data["json"] = result o.Data["json"] = token
}
o.ServeJSON()
}
// @Title Introspection
// @Description introspect token
// @Param body body models.Token true "The token info"
// @Success 200 {string}
// @router /refresh [post]
func (o *OAuthController) Refresh() {
var token auth_connectors.Token
json.Unmarshal(o.Ctx.Input.CopyBody(100000), &token)
// refresh token
newToken, err := infrastructure.GetAuthConnector().Refresh(&token)
if err != nil || newToken == nil {
o.Data["json"] = err
o.Ctx.ResponseWriter.WriteHeader(401)
} else {
o.Data["json"] = newToken
}
o.ServeJSON()
}
// @Title Introspection
// @Description introspect token
// @Param Authorization header string false "auth token"
// @Success 200 {string}
// @router /introspect [get]
func (o *OAuthController) Introspect() {
reqToken := o.Ctx.Request.Header.Get("Authorization")
splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) < 2 {
reqToken = ""
} else {
reqToken = splitToken[1]
}
token, err := infrastructure.GetAuthConnector().Introspect(reqToken)
if err != nil || !token {
o.Data["json"] = err
o.Ctx.ResponseWriter.WriteHeader(401)
} }
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",
} }
// @Title AuthForward // @Title AuthForward
// @Description Forward auth for Traefik — validates JWT via Hydra introspection. // @Description auth forward
// Only requests from our own peer (SELF) are authorized. // @Param Authorization header string false "auth token"
// 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"
// @Success 200 {string} // @Success 200 {string}
// @router /forward [get] // @router /forward [get]
func (o *OAuthController) InternalAuthForward() { func (o *OAuthController) InternalAuthForward() {
fmt.Println("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 == "" {
// Step 1: no token — allow oc-auth's own challenge endpoints (no token needed). for _, w := range whitelist {
// No token and not a whitelisted path → restart OAuth2 flow. if strings.Contains(o.Ctx.Request.Header.Get("X-Forwarded-Uri"), w) {
fmt.Println("NO TOKEN") o.Ctx.ResponseWriter.WriteHeader(200)
o.redirectToLogin(origin) o.ServeJSON()
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 || splitToken[1] == "" { if len(splitToken) < 2 {
fmt.Println("MALFORMED BEARER") reqToken = ""
o.redirectToLogin(origin) } else {
return reqToken = splitToken[1]
} }
reqToken = splitToken[1] origin, publicKey, external := o.extractOrigin()
if !infrastructure.GetAuthConnector().CheckAuthForward( //reqToken != "" &&
// 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"),
uri, external) { o.Ctx.Request.Header.Get("X-Forwarded-Uri"), external) && origin != "" && publicKey != "" {
case http.StatusOK: o.Ctx.ResponseWriter.WriteHeader(401)
fmt.Println("OK") o.ServeJSON()
o.Ctx.ResponseWriter.WriteHeader(http.StatusOK) return
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 func (o *OAuthController) extractOrigin() (string, string, bool) {
// 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) {
user, peerID, groups := oclib.ExtractTokenInfo(*request)
external := true external := true
publicKey := "" publicKey := ""
origin := o.Ctx.Request.Header.Get("X-Forwarded-Host") origin := o.Ctx.Request.Header.Get("X-Forwarded-Host")
@@ -514,15 +174,15 @@ func (o *OAuthController) extractOrigin(request *http.Request) (string, string,
if t != "" { if t != "" {
searchStr = strings.Replace(searchStr, t, "", -1) searchStr = strings.Replace(searchStr, t, "", -1)
} }
pp := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), user, peerID, groups, nil).Search(nil, searchStr, false) peer := oclib.Search(nil, searchStr, oclib.LibDataEnum(oclib.PEER))
if pp.Code != 200 || len(pp.Data) == 0 { if peer.Code != 200 || len(peer.Data) == 0 { // TODO: add state of partnership
return "", "", external return "", "", external
} }
p := pp.Data[0].(*model.Peer) p := peer.Data[0].(*model.Peer)
publicKey = p.PublicKey publicKey = p.PublicKey
origin = p.APIUrl origin = p.Url
if origin != "" { if origin != "" { // is external
if p.Relation == peer.SELF { if strings.Contains(origin, "localhost") || strings.Contains(origin, "127.0.0.1") || p.State == model.SELF {
external = false external = false
} }
} else { } else {
@@ -530,71 +190,3 @@ func (o *OAuthController) extractOrigin(request *http.Request) (string, string,
} }
return origin, publicKey, external return origin, publicKey, external
} }
// ExtractClient extracts the client_id from a JWT token.
// Supports both standard JWT (3 parts with base64 payload) and local dev tokens.
func ExtractClient(request http.Request) string {
reqToken := request.Header.Get("Authorization")
splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) < 2 {
return ""
}
reqToken = splitToken[1]
if reqToken == "" {
return ""
}
// Try to decode as standard JWT (header.payload.signature)
parts := strings.Split(reqToken, ".")
if len(parts) >= 2 {
// Decode the payload (second part of JWT)
payload := parts[1]
// Add padding if needed
switch len(payload) % 4 {
case 2:
payload += "=="
case 3:
payload += "="
}
bytes, err := base64.URLEncoding.DecodeString(payload)
if err != nil {
// Try standard base64 for local dev tokens
bytes, err = base64.StdEncoding.DecodeString(parts[len(parts)-1])
if err != nil {
return ""
}
}
m := map[string]interface{}{}
if err := json.Unmarshal(bytes, &m); err != nil {
return ""
}
// Standard JWT: look for client_id in top-level or ext claims
if cid, ok := m["client_id"].(string); ok {
return cid
}
if ext, ok := m["ext"].(map[string]interface{}); ok {
if cid, ok := ext["client_id"].(string); ok {
return cid
}
}
// Local dev token format: session.id_token.client_id
if session, ok := m["session"].(map[string]interface{}); ok {
if idToken, ok := session["id_token"].(map[string]interface{}); ok {
if cid, ok := idToken["client_id"].(string); ok {
return cid
}
}
}
}
return ""
}
// extractBearerToken extracts the token from the Authorization header
func extractBearerToken(r *http.Request) string {
reqToken := r.Header.Get("Authorization")
splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) < 2 {
return ""
}
return splitToken[1]
}

View File

@@ -16,8 +16,7 @@ type PermissionController struct {
// @Success 200 {permission} string // @Success 200 {permission} string
// @router / [get] // @router / [get]
func (o *PermissionController) GetAll() { func (o *PermissionController) GetAll() {
clientID := ExtractClient(*o.Ctx.Request) role, err := infrastructure.GetPermissionConnector().GetPermission("", "")
role, err := infrastructure.GetPermissionConnector(clientID).GetPermission("", "")
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -42,8 +41,7 @@ func (o *PermissionController) GetAll() {
// @router /role/:id [get] // @router /role/:id [get]
func (o *PermissionController) GetByRole() { func (o *PermissionController) GetByRole() {
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
clientID := ExtractClient(*o.Ctx.Request) role, err := infrastructure.GetPermissionConnector().GetPermissionByRole(id)
role, err := infrastructure.GetPermissionConnector(clientID).GetPermissionByRole(id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -68,8 +66,7 @@ func (o *PermissionController) GetByRole() {
// @router /user/:id [get] // @router /user/:id [get]
func (o *PermissionController) GetByUser() { func (o *PermissionController) GetByUser() {
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
clientID := ExtractClient(*o.Ctx.Request) role, err := infrastructure.GetPermissionConnector().GetPermissionByUser(id, true)
role, err := infrastructure.GetPermissionConnector(clientID).GetPermissionByUser(id, true)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -95,8 +92,7 @@ func (o *PermissionController) GetByUser() {
func (o *PermissionController) Get() { func (o *PermissionController) Get() {
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
rel := o.Ctx.Input.Param(":relation") rel := o.Ctx.Input.Param(":relation")
clientID := ExtractClient(*o.Ctx.Request) role, err := infrastructure.GetPermissionConnector().GetPermission(id, rel)
role, err := infrastructure.GetPermissionConnector(clientID).GetPermission(id, rel)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -119,8 +115,7 @@ func (o *PermissionController) Get() {
// @Success 200 {string} delete success! // @Success 200 {string} delete success!
// @router /clear [delete] // @router /clear [delete]
func (o *PermissionController) Clear() { func (o *PermissionController) Clear() {
clientID := ExtractClient(*o.Ctx.Request) role, code, err := infrastructure.GetPermissionConnector().DeletePermission("", "", true)
role, code, err := infrastructure.GetPermissionConnector(clientID).DeletePermission("", "", true)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -149,8 +144,7 @@ func (o *PermissionController) Bind() {
permission_id := o.Ctx.Input.Param(":permission_id") permission_id := o.Ctx.Input.Param(":permission_id")
role_id := o.Ctx.Input.Param(":role_id") role_id := o.Ctx.Input.Param(":role_id")
rel := o.Ctx.Input.Param(":relation") rel := o.Ctx.Input.Param(":relation")
clientID := ExtractClient(*o.Ctx.Request) role, code, err := infrastructure.GetPermissionConnector().BindPermission(role_id, permission_id, rel)
role, code, err := infrastructure.GetPermissionConnector(clientID).BindPermission(role_id, permission_id, rel)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -179,8 +173,7 @@ func (o *PermissionController) UnBind() {
permission_id := o.Ctx.Input.Param(":permission_id") permission_id := o.Ctx.Input.Param(":permission_id")
role_id := o.Ctx.Input.Param(":role_id") role_id := o.Ctx.Input.Param(":role_id")
rel := o.Ctx.Input.Param(":relation") rel := o.Ctx.Input.Param(":relation")
clientID := ExtractClient(*o.Ctx.Request) role, code, err := infrastructure.GetPermissionConnector().UnBindPermission(role_id, permission_id, rel)
role, code, err := infrastructure.GetPermissionConnector(clientID).UnBindPermission(role_id, permission_id, rel)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,

View File

@@ -19,8 +19,7 @@ type RoleController struct {
func (o *RoleController) Post() { func (o *RoleController) Post() {
// store and return Id or post with UUID // store and return Id or post with UUID
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
clientID := ExtractClient(*o.Ctx.Request) role, code, err := infrastructure.GetPermissionConnector().CreateRole(id)
role, code, err := infrastructure.GetPermissionConnector(clientID).CreateRole(id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -45,8 +44,7 @@ func (o *RoleController) Post() {
// @router /user/:id [get] // @router /user/:id [get]
func (o *RoleController) GetByUser() { func (o *RoleController) GetByUser() {
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
clientID := ExtractClient(*o.Ctx.Request) role, err := infrastructure.GetPermissionConnector().GetRoleByUser(id)
role, err := infrastructure.GetPermissionConnector(clientID).GetRoleByUser(id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -69,8 +67,7 @@ func (o *RoleController) GetByUser() {
// @Success 200 {role} string // @Success 200 {role} string
// @router / [get] // @router / [get]
func (o *RoleController) GetAll() { func (o *RoleController) GetAll() {
clientID := ExtractClient(*o.Ctx.Request) role, err := infrastructure.GetPermissionConnector().GetRole("")
role, err := infrastructure.GetPermissionConnector(clientID).GetRole("")
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -95,8 +92,7 @@ func (o *RoleController) GetAll() {
// @router /:id [get] // @router /:id [get]
func (o *RoleController) Get() { func (o *RoleController) Get() {
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
clientID := ExtractClient(*o.Ctx.Request) role, err := infrastructure.GetPermissionConnector().GetRole(id)
role, err := infrastructure.GetPermissionConnector(clientID).GetRole(id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -121,8 +117,7 @@ func (o *RoleController) Get() {
// @router /:id [delete] // @router /:id [delete]
func (o *RoleController) Delete() { func (o *RoleController) Delete() {
id := o.Ctx.Input.Param(":id") id := o.Ctx.Input.Param(":id")
clientID := ExtractClient(*o.Ctx.Request) role, code, err := infrastructure.GetPermissionConnector().DeleteRole(id)
role, code, err := infrastructure.GetPermissionConnector(clientID).DeleteRole(id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -145,8 +140,7 @@ func (o *RoleController) Delete() {
// @Success 200 {string} delete success! // @Success 200 {string} delete success!
// @router /clear [delete] // @router /clear [delete]
func (o *RoleController) Clear() { func (o *RoleController) Clear() {
clientID := ExtractClient(*o.Ctx.Request) role, code, err := infrastructure.GetPermissionConnector().DeleteRole("")
role, code, err := infrastructure.GetPermissionConnector(clientID).DeleteRole("")
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -173,8 +167,7 @@ func (o *RoleController) Clear() {
func (o *RoleController) Bind() { func (o *RoleController) Bind() {
user_id := o.Ctx.Input.Param(":user_id") user_id := o.Ctx.Input.Param(":user_id")
role_id := o.Ctx.Input.Param(":role_id") role_id := o.Ctx.Input.Param(":role_id")
clientID := ExtractClient(*o.Ctx.Request) role, code, err := infrastructure.GetPermissionConnector().BindRole(user_id, role_id)
role, code, err := infrastructure.GetPermissionConnector(clientID).BindRole(user_id, role_id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,
@@ -201,8 +194,7 @@ func (o *RoleController) Bind() {
func (o *RoleController) UnBind() { func (o *RoleController) UnBind() {
user_id := o.Ctx.Input.Param(":user_id") user_id := o.Ctx.Input.Param(":user_id")
role_id := o.Ctx.Input.Param(":role_id") role_id := o.Ctx.Input.Param(":role_id")
clientID := ExtractClient(*o.Ctx.Request) role, code, err := infrastructure.GetPermissionConnector().UnBindRole(user_id, role_id)
role, code, err := infrastructure.GetPermissionConnector(clientID).UnBindRole(user_id, role_id)
if err != nil { if err != nil {
o.Data["json"] = map[string]interface{}{ o.Data["json"] = map[string]interface{}{
"data": nil, "data": nil,

View File

@@ -14,10 +14,7 @@ type VersionController struct {
// @Success 200 // @Success 200
// @router / [get] // @router / [get]
func (c *VersionController) GetAll() { func (c *VersionController) GetAll() {
c.Data["json"] = map[string]string{ c.Data["json"] = map[string]string{"version": "1"}
"service": "oc-auth",
"version": "1",
}
c.ServeJSON() c.ServeJSON()
} }
@@ -26,9 +23,6 @@ func (c *VersionController) GetAll() {
// @Success 200 // @Success 200
// @router /discovery [get] // @router /discovery [get]
func (c *VersionController) Get() { func (c *VersionController) Get() {
c.Data["json"] = map[string]string{ c.Data["json"] = map[string]string{"version": "1"}
"service": "oc-auth",
"version": "1",
}
c.ServeJSON() c.ServeJSON()
} }

View File

@@ -1,6 +1,22 @@
version: '3.4' version: '3.4'
services: services:
traefik:
image: traefik:v2.10.4
container_name: traefik
networks:
- catalog
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--log.level=DEBUG"
ports:
- "8080:80"
- "8082:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
oc-auth: oc-auth:
image: 'oc-auth:latest' image: 'oc-auth:latest'
ports: ports:
@@ -8,27 +24,18 @@ services:
container_name: oc-auth container_name: oc-auth
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.auth-sec.entrypoints=web" - "traefik.http.middlewares.auth.forwardauth.address=http://oc-auth:8080/oc/forward"
- "traefik.http.routers.auth-sec.rule=PathPrefix(`/auth/`)" - "traefik.http.routers.workflow.rule=PathPrefix(/auth)"
- "traefik.http.middlewares.auth-sec-rewrite.replacepathregex.regex=^/auth(.*)"
- "traefik.http.middlewares.auth-sec-rewrite.replacepathregex.replacement=/oc$$1"
- "traefik.http.services.auth-sec.loadbalancer.server.port=8080"
- "traefik.http.routers.auth-sec.middlewares=auth-sec-rewrite,auth-auth-sec"
- "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
LDAP_BINDPW: password LDAP_BINDPW: password
LDAP_BASEDN: "dc=example,dc=com" LDAP_BASEDN: "dc=example,dc=com"
LDAP_USER_BASEDN: "ou=users,dc=example,dc=com"
LDAP_ROLE_BASEDN: "ou=AppRoles,dc=example,dc=com" LDAP_ROLE_BASEDN: "ou=AppRoles,dc=example,dc=com"
networks: networks:
- oc - catalog
volumes: volumes:
- ./pem/private.pem:/keys/private/private.pem - ./pem:/etc/oc/pem
- ./pem/public.pem:/keys/public/public.pem
networks: networks:
oc: catalog:
external: true external: true

View File

@@ -2,10 +2,9 @@
"MONGO_URL":"mongodb://mongo:27017/", "MONGO_URL":"mongodb://mongo:27017/",
"MONGO_DATABASE":"DC_myDC", "MONGO_DATABASE":"DC_myDC",
"NATS_URL": "nats://nats:4222", "NATS_URL": "nats://nats:4222",
"PORT" : 8080,
"AUTH_CONNECTOR_HOST": "hydra", "AUTH_CONNECTOR_HOST": "hydra",
"AUTH_CONNECTOR_PUBLIC_HOST": "hydra", "PRIVATE_KEY_PATH": "/etc/oc/pem/private.pem",
"PRIVATE_KEY_PATH": "/keys/private/private.pem", "PUBLIC_KEY_PATH": "/etc/oc/pem/public.pem",
"PUBLIC_KEY_PATH": "/keys/public/public.pem", "LDAP_ENDPOINTS": "ldap:389"
"LDAP_ENDPOINTS": "ldap:389",
"LOCAL": false
} }

View File

@@ -1,4 +0,0 @@
KUBERNETES_SERVICE_HOST=192.168.47.20
KUBE_CA="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJkekNDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUzTWpNeE1USXdNell3SGhjTk1qUXdPREE0TVRBeE16VTJXaGNOTXpRd09EQTJNVEF4TXpVMgpXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUzTWpNeE1USXdNell3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFTVlk3ZHZhNEdYTVdkMy9jMlhLN3JLYjlnWXgyNSthaEE0NmkyNVBkSFAKRktQL2UxSVMyWVF0dzNYZW1TTUQxaStZdzJSaVppNUQrSVZUamNtNHdhcnFvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVWtlUVJpNFJiODduME5yRnZaWjZHClc2SU55NnN3Q2dZSUtvWkl6ajBFQXdJRFNBQXdSUUlnRXA5ck04WmdNclRZSHYxZjNzOW5DZXZZeWVVa3lZUk4KWjUzazdoaytJS1FDSVFDbk05TnVGKzlTakIzNDFacGZ5ays2NEpWdkpSM3BhcmVaejdMd2lhNm9kdz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
KUBE_CERT="LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENDQVRlZ0F3SUJBZ0lJWUxWNkFPQkdrU1F3Q2dZSUtvWkl6ajBFQXdJd0l6RWhNQjhHQTFVRUF3d1kKYXpOekxXTnNhV1Z1ZEMxallVQXhOekl6TVRFeU1ETTJNQjRYRFRJME1EZ3dPREV3TVRNMU5sb1hEVEkxTURndwpPREV3TVRNMU5sb3dNREVYTUJVR0ExVUVDaE1PYzNsemRHVnRPbTFoYzNSbGNuTXhGVEFUQmdOVkJBTVRESE41CmMzUmxiVHBoWkcxcGJqQlpNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQTBJQUJGQ2Q1MFdPeWdlQ2syQzcKV2FrOWY4MVAvSkJieVRIajRWOXBsTEo0ck5HeHFtSjJOb2xROFYxdUx5RjBtOTQ2Nkc0RmRDQ2dqaXFVSk92Swp3NVRPNnd5alNEQkdNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRkJRY0RBakFmCkJnTlZIU01FR0RBV2dCVFJkOFI5cXVWK2pjeUVmL0ovT1hQSzMyS09XekFLQmdncWhrak9QUVFEQWdOSUFEQkYKQWlFQTArbThqTDBJVldvUTZ0dnB4cFo4NVlMalF1SmpwdXM0aDdnSXRxS3NmUVVDSUI2M2ZNdzFBMm5OVWU1TgpIUGZOcEQwSEtwcVN0Wnk4djIyVzliYlJUNklZCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdFkyeHAKWlc1MExXTmhRREUzTWpNeE1USXdNell3SGhjTk1qUXdPREE0TVRBeE16VTJXaGNOTXpRd09EQTJNVEF4TXpVMgpXakFqTVNFd0h3WURWUVFEREJock0zTXRZMnhwWlc1MExXTmhRREUzTWpNeE1USXdNell3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFRc3hXWk9pbnIrcVp4TmFEQjVGMGsvTDF5cE01VHAxOFRaeU92ektJazQKRTFsZWVqUm9STW0zNmhPeVljbnN3d3JoNnhSUnBpMW5RdGhyMzg0S0Z6MlBvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVTBYZkVmYXJsZm8zTWhIL3lmemx6Cnl0OWlqbHN3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQUxJL2dNYnNMT3MvUUpJa3U2WHVpRVMwTEE2cEJHMXgKcnBlTnpGdlZOekZsQWlFQW1wdjBubjZqN3M0MVI0QzFNMEpSL0djNE53MHdldlFmZWdEVGF1R2p3cFk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
KUBE_DATA="LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSU5ZS1BFb1dhd1NKUzJlRW5oWmlYMk5VZlY1ZlhKV2krSVNnV09TNFE5VTlvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFVUozblJZN0tCNEtUWUx0WnFUMS96VS84a0Z2Sk1lUGhYMm1Vc25pczBiR3FZblkyaVZEeApYVzR2SVhTYjNqcm9iZ1YwSUtDT0twUWs2OHJEbE03ckRBPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo="

37
go.mod
View File

@@ -1,27 +1,22 @@
module oc-auth module oc-auth
go 1.24.6 go 1.22.0
require ( require (
cloud.o-forge.io/core/oc-lib v0.0.0-20260219084344-9662ac6d678c cloud.o-forge.io/core/oc-lib v0.0.0-20241216081858-245f3adea3ba
github.com/beego/beego/v2 v2.3.1 github.com/beego/beego/v2 v2.3.4
github.com/smartystreets/goconvey v1.7.2 github.com/smartystreets/goconvey v1.7.2
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
) )
//replace cloud.o-forge.io/core/oc-lib => ../oc-lib replace cloud.o-forge.io/core/oc-lib => ../oc-lib
require ( require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/biter777/countries v1.7.5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/gofrs/uuid v4.3.0+incompatible // indirect github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/libp2p/go-libp2p/core v0.43.0-rc2 // indirect github.com/nats-io/nats.go v1.38.0 // indirect
github.com/nats-io/nats.go v1.37.0 // indirect github.com/robfig/cron v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
) )
@@ -29,11 +24,11 @@ require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coocood/freecache v1.2.4 github.com/coocood/freecache v1.2.4
github.com/gabriel-vasile/mimetype v1.4.6 // indirect github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/go-ldap/ldap/v3 v3.4.8 github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.1 // indirect github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
@@ -48,11 +43,11 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect github.com/montanaflynn/stats v0.7.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nkeys v0.4.9 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.1 // indirect github.com/prometheus/common v0.61.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rs/zerolog v1.33.0 // indirect github.com/rs/zerolog v1.33.0 // indirect
github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 // indirect github.com/shiena/ansicolor v0.0.0-20230509054315-a9deabde6e02 // indirect
@@ -62,11 +57,11 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver v1.17.1 // indirect go.mongodb.org/mongo-driver v1.17.1 // indirect
golang.org/x/crypto v0.39.0 // indirect golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.30.0 // indirect golang.org/x/net v0.33.0 // indirect
golang.org/x/sync v0.15.0 // indirect golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.26.0 // indirect golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

85
go.sum
View File

@@ -1,22 +1,12 @@
cloud.o-forge.io/core/oc-lib v0.0.0-20260204083845-d9f646aac28b h1:/TkmuO5ERpHJCqNpKBlmzw8pYTVDGcFcDo+e1ndXlm0=
cloud.o-forge.io/core/oc-lib v0.0.0-20260204083845-d9f646aac28b/go.mod h1:T0UCxRd8w+qCVVC0NEyDiWIGC5ADwEbQ7hFcvftd4Ks=
cloud.o-forge.io/core/oc-lib v0.0.0-20260210081202-3bcf0da56aa1 h1:CSPqJlSepu0efDRFV8tv62Fg5XP2UwSZKfaaL81YuVY=
cloud.o-forge.io/core/oc-lib v0.0.0-20260210081202-3bcf0da56aa1/go.mod h1:jmyBwmsac/4V7XPL347qawF60JsBCDmNAMfn/ySXKYo=
cloud.o-forge.io/core/oc-lib v0.0.0-20260212123952-403913d8cf13 h1:DNIPQ7C+7wjbj5RUx29wLxuIe/wiSOcuUMlLRIv6Fvs=
cloud.o-forge.io/core/oc-lib v0.0.0-20260212123952-403913d8cf13/go.mod h1:jmyBwmsac/4V7XPL347qawF60JsBCDmNAMfn/ySXKYo=
cloud.o-forge.io/core/oc-lib v0.0.0-20260219084344-9662ac6d678c h1:brsB6se+xMv386Vf6dSu3In2QZSH4EqgcAYkI4fNpJw=
cloud.o-forge.io/core/oc-lib v0.0.0-20260219084344-9662ac6d678c/go.mod h1:jmyBwmsac/4V7XPL347qawF60JsBCDmNAMfn/ySXKYo=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/beego/beego/v2 v2.3.1 h1:7MUKMpJYzOXtCUsTEoXOxsDV/UcHw6CPbaWMlthVNsc= github.com/beego/beego/v2 v2.3.4 h1:HurQEOGIEhLlPFCTR6ZDuQkybrUl2Ag2i6CdVD2rGiI=
github.com/beego/beego/v2 v2.3.1/go.mod h1:5cqHsOHJIxkq44tBpRvtDe59GuVRVv/9/tyVDxd5ce4= github.com/beego/beego/v2 v2.3.4/go.mod h1:5cqHsOHJIxkq44tBpRvtDe59GuVRVv/9/tyVDxd5ce4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/biter777/countries v1.7.5 h1:MJ+n3+rSxWQdqVJU8eBy9RqcdH6ePPn4PJHocVWUa+Q=
github.com/biter777/countries v1.7.5/go.mod h1:1HSpZ526mYqKJcpT5Ti1kcGQ0L0SrXWIaptUWjFfv2E=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -26,17 +16,14 @@ github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw= github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/etcd-io/etcd v3.3.17+incompatible/go.mod h1:cdZ77EstHBwVtD6iTgzgvogwcjo9m4iOqoijouPJ4bs= github.com/etcd-io/etcd v3.3.17+incompatible/go.mod h1:cdZ77EstHBwVtD6iTgzgvogwcjo9m4iOqoijouPJ4bs=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
@@ -47,8 +34,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc=
@@ -99,8 +86,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/libp2p/go-libp2p/core v0.43.0-rc2 h1:1X1aDJNWhMfodJ/ynbaGLkgnC8f+hfBIqQDrzxFZOqI=
github.com/libp2p/go-libp2p/core v0.43.0-rc2/go.mod h1:NYeJ9lvyBv9nbDk2IuGb8gFKEOkIv/W5YRIy1pAJB2Q=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -119,30 +104,30 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE= github.com/nats-io/nats.go v1.38.0 h1:A7P+g7Wjp4/NWqDOOP/K6hfhr54DvdDQUznt5JFg9XA=
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nats.go v1.38.0/go.mod h1:IGUM++TwokGnXPs82/wCuiHS02/aKrdYUQkU8If6yjw=
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0=
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= github.com/ogier/pflag v0.0.1/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPAaSc= github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@@ -191,10 +176,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -208,15 +191,13 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -231,10 +212,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -248,20 +227,16 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -1,123 +1,41 @@
package auth_connectors package auth_connectors
import ( import (
"net/http"
"oc-auth/conf" "oc-auth/conf"
"oc-auth/infrastructure/claims"
"strings"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type AuthConnector interface { type AuthConnector interface {
Status() tools.State Status() tools.State
Login(username string, cookies ...*http.Cookie) (*Token, error)
// Login/Consent Provider endpoints (Hydra redirects here) Logout(token string, cookies ...*http.Cookie) (*Token, error)
GetLoginChallenge(challenge string) (*LoginChallenge, error) Introspect(token string, cookie ...*http.Cookie) (bool, error)
AcceptLogin(challenge string, subject string) (*Redirect, error) Refresh(token *Token) (*Token, error)
RejectLogin(challenge string, reason string) (*Redirect, error) CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) bool
GetConsentChallenge(challenge string) (*ConsentChallenge, error)
AcceptConsent(challenge string, grantScope []string, session claims.Claims) (*Redirect, error)
// Logout Provider endpoints (Hydra redirects here)
GetLogoutChallenge(challenge string) (*LogoutChallenge, error)
AcceptLogout(challenge string) (*Redirect, error)
// Token operations
Introspect(token string) (*IntrospectResult, error)
RevokeToken(token string, clientID string) error
RefreshToken(refreshToken string, clientID string) (*TokenResponse, error)
// CheckAuthForward validates the token and permissions for a forward auth request.
// 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
type Token struct { type Token struct {
Active bool `json:"active"` Active bool `json:"active"`
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"` ExpiresIn int64 `json:"expires_in"`
IDToken string `json:"id_token,omitempty"` TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
TokenType string `json:"token_type"` Username string `json:"username,omitempty"`
Scope string `json:"scope,omitempty"` Password string `json:"password,omitempty"`
} }
// LoginRequest is the body of POST /oc/login
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
LoginChallenge string `json:"login_challenge"`
}
// Redirect is a response containing a redirect URL from Hydra
type Redirect struct { type Redirect struct {
RedirectTo string `json:"redirect_to"` RedirectTo string `json:"redirect_to"`
} }
// LoginChallenge contains the details of a Hydra login challenge
type LoginChallenge struct {
Skip bool `json:"skip"`
Subject string `json:"subject"`
Challenge string `json:"challenge"`
Client map[string]interface{} `json:"client"`
RequestURL string `json:"request_url"`
SessionID string `json:"session_id"`
}
// LogoutChallenge contains the details of a Hydra logout challenge
type LogoutChallenge struct {
Subject string `json:"subject"`
SessionID string `json:"sid"`
RequestURL string `json:"request_url"`
RPInitiated bool `json:"rp_initiated"`
}
// ConsentChallenge contains the details of a Hydra consent challenge
type ConsentChallenge struct {
Skip bool `json:"skip"`
Subject string `json:"subject"`
Challenge string `json:"challenge"`
RequestedScope []string `json:"requested_scope"`
RequestedAccessTokenAud []string `json:"requested_access_token_audience"`
Client map[string]interface{} `json:"client"`
}
// TokenResponse is the OAuth2 token response from Hydra
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
IDToken string `json:"id_token,omitempty"`
Scope string `json:"scope"`
}
// IntrospectResult is the OAuth2 introspection response from Hydra
type IntrospectResult struct {
Active bool `json:"active"`
Sub string `json:"sub,omitempty"`
ClientID string `json:"client_id,omitempty"`
Scope string `json:"scope,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
TokenType string `json:"token_type,omitempty"`
Extra map[string]interface{} `json:"ext,omitempty"`
}
var a = map[string]AuthConnector{ var a = map[string]AuthConnector{
"hydra": &HydraConnector{ "hydra": HydraConnector{
Caller: tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{}), Caller: tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{}),
}, State: "12345678", ResponseType: "token", Scopes: "openid profile email roles"}, // base url
} }
func GetAuthConnector() AuthConnector { func GetAuthConnector() AuthConnector {
for k := range a { return a[conf.GetConfig().Auth]
if strings.Contains(conf.GetConfig().Auth, k) {
return a[k]
}
}
return nil
} }

View File

@@ -1,6 +1,7 @@
package auth_connectors package auth_connectors
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -9,7 +10,10 @@ import (
"net/url" "net/url"
"oc-auth/conf" "oc-auth/conf"
"oc-auth/infrastructure/claims" "oc-auth/infrastructure/claims"
"regexp"
"strconv"
"strings" "strings"
"time"
oclib "cloud.o-forge.io/core/oc-lib" oclib "cloud.o-forge.io/core/oc-lib"
"cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/models/peer"
@@ -17,16 +21,21 @@ import (
) )
type HydraConnector struct { type HydraConnector struct {
State string `json:"state"`
Scopes string `json:"scope"`
ClientID string `json:"client_id"`
ResponseType string `json:"response_type"`
Caller *tools.HTTPCaller Caller *tools.HTTPCaller
} }
func (h *HydraConnector) Status() tools.State { const test_name = "test-pierre"
const test_id = "1234"
func (a HydraConnector) Status() tools.State {
caller := tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{}) caller := tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{})
var responseBody map[string]interface{} var responseBody map[string]interface{}
host := conf.GetConfig().AuthConnectPublicHost host := conf.GetConfig().AuthConnectorHost
if conf.GetConfig().Local {
host = "localhost"
}
port := fmt.Sprintf("%v", conf.GetConfig().AuthConnectorPort) port := fmt.Sprintf("%v", conf.GetConfig().AuthConnectorPort)
resp, err := caller.CallGet("http://"+host+":"+port, "/health/ready") resp, err := caller.CallGet("http://"+host+":"+port, "/health/ready")
if err != nil { if err != nil {
@@ -39,328 +48,237 @@ func (h *HydraConnector) Status() tools.State {
return tools.ALIVE return tools.ALIVE
} }
// getPath builds the base URL for Hydra API calls
func (h *HydraConnector) getPath(isAdmin bool, isOauth bool) string { // urlFormat formats the URL of the peer with the data type API function
host := conf.GetConfig().AuthConnectPublicHost func (a *HydraConnector) urlFormat(url string, replaceWith string) string {
if isAdmin { // localhost is replaced by the local peer URL
host = conf.GetConfig().AuthConnectorHost // because localhost must collide on a web request security protocol
r := regexp.MustCompile("(http://[a-z]+:[0-9]+)/oauth2")
t := r.FindString(url)
if t != "" {
url = strings.Replace(url, t, replaceWith, -1)
} }
if conf.GetConfig().Local { return url
host = "localhost" }
func (a HydraConnector) challenge(username string, url string, challenge string, cookies ...*http.Cookie) (*Redirect, string, []*http.Cookie, error) {
body := map[string]interface{}{
"remember_for": 0,
"remember": true,
} }
if challenge != "consent" {
body["subject"] = username
}
s := strings.Split(url, challenge+"_challenge=")
resp, err := a.Caller.CallRaw(http.MethodPut,
a.getPath(true, true), "/auth/requests/"+challenge+"/accept?"+challenge+"_challenge="+s[1],
body, "application/json", true, cookies...) // "remember": true, "subject": username
if err != nil {
return nil, s[1], cookies, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, s[1], cookies, err
}
var token Redirect
err = json.Unmarshal(b, &token)
if err != nil {
return nil, s[1], cookies, err
}
return &token, s[1], cookies, nil
}
func (a HydraConnector) Refresh(token *Token) (*Token, error) {
access := strings.Split(token.AccessToken, ".")
if len(access) > 2 {
token.AccessToken = strings.Join(access[0:2], ".")
}
isValid, err := a.Introspect(token.AccessToken)
if err != nil || !isValid {
return nil, err
}
_, err = a.Logout(token.AccessToken)
if err != nil {
return nil, err
}
return a.Login(token.Username)
}
func (a HydraConnector) tryLog(username string, url string, subpath string, challenge string, cookies ...*http.Cookie) (*Redirect, string, []*http.Cookie, error) {
resp, err := a.Caller.CallRaw(http.MethodGet, url, subpath,
map[string]interface{}{}, "application/json", true, cookies...)
if err != nil || resp.Request.Response == nil || resp.Request.Response.Header["Set-Cookie"] == nil {
return nil, "", cookies, err
}
cc := resp.Request.Response.Header["Set-Cookie"] // retrieve oauth2 csrf token cookie
if len(cc) > 0 {
for _, c := range cc {
first := strings.Split(c, ";")
cookies = append(cookies, &http.Cookie{
Name: strings.Split(first[0], "=")[0],
Value: strings.ReplaceAll(first[0], strings.Split(first[0], "=")[0]+"=", ""),
})
}
}
return a.challenge(username, resp.Request.URL.String(), challenge, cookies...)
}
func (a HydraConnector) getClient() string {
resp, err := a.Caller.CallGet(a.getPath(true, false), "/clients")
if err != nil {
return ""
}
var clients []interface{}
err = json.Unmarshal(resp, &clients)
if err != nil || len(clients) == 0 {
return ""
}
return clients[0].(map[string]interface{})["client_id"].(string)
}
func (a HydraConnector) Login(username string, cookies ...*http.Cookie) (t *Token, err error) {
clientID := a.getClient()
redirect, _, cookies, err := a.tryLog(username, a.getPath(false, true),
"/auth?client_id="+clientID+"&response_type="+strings.ReplaceAll(a.ResponseType, " ", "%20")+"&scope="+strings.ReplaceAll(a.Scopes, " ", "%20")+"&state="+a.State,
"login", cookies...)
if err != nil || redirect == nil {
return nil, err
}
redirect, _, cookies, err = a.tryLog(username, a.urlFormat(redirect.RedirectTo, a.getPath(false, true)), "", "consent", cookies...)
if err != nil || redirect == nil {
return nil, err
}
// problem with consent THERE we need to accept the consent challenge && get the token
_, err = a.Caller.CallRaw(http.MethodGet, a.urlFormat(redirect.RedirectTo, a.getPath(false, true)), "", map[string]interface{}{},
"application/json", true, cookies...)
if err != nil {
s := strings.Split(err.Error(), "\"")
if len(s) > 1 && strings.Contains(s[1], "access_token") {
err = nil
} else {
return nil, err
}
}
token := &Token{
Username: username,
}
urls := url.Values{}
urls.Add("client_id", clientID)
urls.Add("client_secret", conf.GetConfig().ClientSecret)
urls.Add("grant_type", "client_credentials")
resp, err := a.Caller.CallForm(http.MethodPost, a.getPath(false, true), "/token", urls,
"application/x-www-form-urlencoded", true, cookies...)
var m map[string]interface{}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(b, &token)
if err != nil {
return nil, err
}
json.Unmarshal(b, &m)
pp := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER),test_name,test_id,nil,nil).Search(nil, strconv.Itoa(peer.SELF.EnumIndex()))
if len(pp.Data) == 0 || pp.Code >= 300 || pp.Err != "" {
return nil, errors.New("peer not found")
}
now := time.Now().UTC()
now = now.Add(time.Duration(token.ExpiresIn) * time.Second)
unix := now.Unix()
c := claims.GetClaims().AddClaimsToToken(username, pp.Data[0].(*peer.Peer))
c.Session.AccessToken["exp"] = unix
b, _ = json.Marshal(c)
token.AccessToken = strings.ReplaceAll(token.AccessToken, "ory_at_", "") + "." + base64.StdEncoding.EncodeToString(b)
token.Active = true
return token, nil
}
func (a HydraConnector) Logout(token string, cookies ...*http.Cookie) (*Token, error) {
access := strings.Split(token, ".")
if len(access) > 2 {
token = strings.Join(access[0:2], ".")
}
p := a.getPath(false, true) + "/revoke"
urls := url.Values{}
urls.Add("token", token)
urls.Add("client_id", a.getClient())
urls.Add("client_secret", conf.GetConfig().ClientSecret)
_, err := a.Caller.CallForm(http.MethodPost, p, "", urls, "application/x-www-form-urlencoded", true)
if err != nil {
return nil, err
}
return &Token{
AccessToken: token,
Active: false,
}, nil
}
func (a HydraConnector) Introspect(token string, cookie ...*http.Cookie) (bool, error) {
// check validity of the token by calling introspect endpoint
// if token is not active, we need to re-authenticate by sending the user to the login page
access := strings.Split(token, ".")
if len(access) > 2 {
token = strings.Join(access[0:2], ".")
}
urls := url.Values{}
urls.Add("token", token)
resp, err := a.Caller.CallForm(http.MethodPost, a.getPath(true, true), "/introspect", urls,
"application/x-www-form-urlencoded", true, cookie...)
if err != nil || resp.StatusCode >= 300 {
return false, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return false, err
}
var introspect Token
err = json.Unmarshal(b, &introspect)
if err != nil {
return false, err
}
introspect.AccessToken = token
return introspect.Active, nil
}
func (a HydraConnector) getPath(isAdmin bool, isOauth bool) string {
host := conf.GetConfig().AuthConnectorHost
port := fmt.Sprintf("%v", conf.GetConfig().AuthConnectorPort) port := fmt.Sprintf("%v", conf.GetConfig().AuthConnectorPort)
if isAdmin { if isAdmin {
port = fmt.Sprintf("%v", conf.GetConfig().AuthConnectorAdminPort) port = fmt.Sprintf("%v", conf.GetConfig().AuthConnectorAdminPort) + "/admin"
} }
oauth := "" oauth := ""
if isOauth { if isOauth {
oauth = "/oauth2" oauth = "/oauth2"
} }
return "http://" + host + ":" + port + oauth return "http://" + host + ":" + port + oauth
} }
// GetLoginChallenge retrieves login challenge details from Hydra admin API func (a HydraConnector) CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) bool {
func (h *HydraConnector) GetLoginChallenge(challenge string) (*LoginChallenge, error) {
logger := oclib.GetLogger()
resp, err := h.Caller.CallGet(h.getPath(true, true), "/auth/requests/login?login_challenge="+url.QueryEscape(challenge))
if err != nil {
logger.Error().Msg("Failed to get login challenge: " + err.Error())
return nil, err
}
var result LoginChallenge
if err := json.Unmarshal(resp, &result); err != nil {
logger.Error().Msg("Failed to unmarshal login challenge: " + err.Error())
return nil, err
}
return &result, nil
}
// AcceptLogin accepts a login challenge after LDAP authentication
func (h *HydraConnector) AcceptLogin(challenge string, subject string) (*Redirect, error) {
logger := oclib.GetLogger()
body := map[string]interface{}{
"subject": subject,
"remember": true,
"remember_for": 3600,
}
resp, err := h.Caller.CallRaw(http.MethodPut,
h.getPath(true, true), "/auth/requests/login/accept?login_challenge="+url.QueryEscape(challenge),
body, "application/json", true)
if err != nil {
logger.Error().Msg("Failed to accept login challenge: " + err.Error())
return nil, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
return nil, errors.New("hydra accept login returned status " + resp.Status + ": " + string(b))
}
var redirect Redirect
if err := json.Unmarshal(b, &redirect); err != nil {
return nil, err
}
return &redirect, nil
}
// RejectLogin rejects a login challenge
func (h *HydraConnector) RejectLogin(challenge string, reason string) (*Redirect, error) {
logger := oclib.GetLogger()
body := map[string]interface{}{
"error": "access_denied",
"error_description": reason,
}
resp, err := h.Caller.CallRaw(http.MethodPut,
h.getPath(true, true), "/auth/requests/login/reject?login_challenge="+url.QueryEscape(challenge),
body, "application/json", true)
if err != nil {
logger.Error().Msg("Failed to reject login challenge: " + err.Error())
return nil, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var redirect Redirect
if err := json.Unmarshal(b, &redirect); err != nil {
return nil, err
}
return &redirect, nil
}
// GetLogoutChallenge retrieves logout challenge details from Hydra admin API
func (h *HydraConnector) GetLogoutChallenge(challenge string) (*LogoutChallenge, error) {
logger := oclib.GetLogger()
resp, err := h.Caller.CallGet(h.getPath(true, true), "/auth/requests/logout?logout_challenge="+url.QueryEscape(challenge))
if err != nil {
logger.Error().Msg("Failed to get logout challenge: " + err.Error())
return nil, err
}
var result LogoutChallenge
if err := json.Unmarshal(resp, &result); err != nil {
logger.Error().Msg("Failed to unmarshal logout challenge: " + err.Error())
return nil, err
}
return &result, nil
}
// AcceptLogout accepts a logout challenge — invalidates the Hydra session
func (h *HydraConnector) AcceptLogout(challenge string) (*Redirect, error) {
logger := oclib.GetLogger()
resp, err := h.Caller.CallRaw(http.MethodPut,
h.getPath(true, true), "/auth/requests/logout/accept?logout_challenge="+url.QueryEscape(challenge),
nil, "application/json", true)
if err != nil {
logger.Error().Msg("Failed to accept logout challenge: " + err.Error())
return nil, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
return nil, errors.New("hydra accept logout returned status " + resp.Status + ": " + string(b))
}
var redirect Redirect
if err := json.Unmarshal(b, &redirect); err != nil {
return nil, err
}
return &redirect, nil
}
// GetConsentChallenge retrieves consent challenge details from Hydra admin API
func (h *HydraConnector) GetConsentChallenge(challenge string) (*ConsentChallenge, error) {
logger := oclib.GetLogger()
resp, err := h.Caller.CallGet(h.getPath(true, true), "/auth/requests/consent?consent_challenge="+url.QueryEscape(challenge))
if err != nil {
logger.Error().Msg("Failed to get consent challenge: " + err.Error())
return nil, err
}
var result ConsentChallenge
if err := json.Unmarshal(resp, &result); err != nil {
logger.Error().Msg("Failed to unmarshal consent challenge: " + err.Error())
return nil, err
}
return &result, nil
}
// AcceptConsent accepts a consent challenge with claims injected into the Hydra session
func (h *HydraConnector) AcceptConsent(challenge string, grantScope []string, session claims.Claims) (*Redirect, error) {
logger := oclib.GetLogger()
body := map[string]interface{}{
"grant_scope": grantScope,
"grant_access_token_audience": grantScope, // grant requested audience
"remember": true,
"remember_for": 3600,
"session": map[string]interface{}{
"access_token": session.Session.AccessToken,
"id_token": session.Session.IDToken,
},
}
resp, err := h.Caller.CallRaw(http.MethodPut,
h.getPath(true, true), "/auth/requests/consent/accept?consent_challenge="+url.QueryEscape(challenge),
body, "application/json", true)
if err != nil {
logger.Error().Msg("Failed to accept consent challenge: " + err.Error())
return nil, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
return nil, errors.New("hydra accept consent returned status " + resp.Status + ": " + string(b))
}
var redirect Redirect
if err := json.Unmarshal(b, &redirect); err != nil {
return nil, err
}
return &redirect, nil
}
// Introspect verifies a token with Hydra — respects the actual response (no override)
func (h *HydraConnector) Introspect(token string) (*IntrospectResult, error) {
logger := oclib.GetLogger()
urls := url.Values{}
urls.Add("token", token)
resp, err := h.Caller.CallForm(http.MethodPost, h.getPath(true, true), "/introspect", urls,
"application/x-www-form-urlencoded", true)
if err != nil {
logger.Error().Msg("Failed to introspect token: " + err.Error())
return nil, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
return nil, errors.New("hydra introspect returned status " + resp.Status)
}
var result IntrospectResult
if err := json.Unmarshal(b, &result); err != nil {
return nil, err
}
return &result, nil
}
// RevokeToken revokes an OAuth2 token
func (h *HydraConnector) RevokeToken(token string, clientID string) error {
logger := oclib.GetLogger()
urls := url.Values{}
urls.Add("token", token)
urls.Add("client_id", clientID)
urls.Add("client_secret", conf.GetConfig().ClientSecret)
resp, err := h.Caller.CallForm(http.MethodPost, h.getPath(false, true), "/revoke", urls,
"application/x-www-form-urlencoded", true)
if err != nil {
logger.Error().Msg("Failed to revoke token: " + err.Error())
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
b, _ := io.ReadAll(resp.Body)
return errors.New("hydra revoke returned status " + resp.Status + ": " + string(b))
}
return nil
}
// RefreshToken exchanges a refresh_token for a new token set
func (h *HydraConnector) RefreshToken(refreshToken string, clientID string) (*TokenResponse, error) {
logger := oclib.GetLogger()
urls := url.Values{}
urls.Add("grant_type", "refresh_token")
urls.Add("refresh_token", refreshToken)
urls.Add("client_id", clientID)
urls.Add("client_secret", conf.GetConfig().ClientSecret)
resp, err := h.Caller.CallForm(http.MethodPost, h.getPath(false, true), "/token", urls,
"application/x-www-form-urlencoded", true)
if err != nil {
logger.Error().Msg("Failed to refresh token: " + err.Error())
return nil, err
}
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 300 {
return nil, errors.New("hydra refresh returned status " + resp.Status + ": " + string(b))
}
var result TokenResponse
if err := json.Unmarshal(b, &result); err != nil {
return nil, err
}
return &result, nil
}
// CheckAuthForward validates a JWT token for forward auth (Traefik integration).
// It introspects the token via Hydra then checks permissions via Keto.
// 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 http.StatusUnauthorized return false
} }
// Defense in depth: only SELF peer requests are allowed. var c claims.Claims
if external { token := strings.Split(reqToken, ".")
return http.StatusUnauthorized if len(token) > 2 {
} bytes, err := base64.StdEncoding.DecodeString(token[2])
logger := oclib.GetLogger()
// Introspect the token via Hydra.
// An inactive or invalid token means the user must re-authenticate → 401.
result, err := h.Introspect(reqToken)
if err != nil || !result.Active {
if err != nil { if err != nil {
logger.Error().Msg("Forward auth introspect failed: " + err.Error()) return false
} }
return http.StatusUnauthorized err = json.Unmarshal(bytes, &c)
} if err != nil {
return false
// Build session claims from Hydra's introspection "ext" field.
// Hydra injects the consent session's access_token data there.
var sessionClaims claims.Claims
sessionClaims.Session.AccessToken = make(map[string]interface{})
sessionClaims.Session.IDToken = make(map[string]interface{})
if result.Extra != nil {
for k, v := range result.Extra {
sessionClaims.Session.AccessToken[k] = v
} }
} }
// ask keto for permission is in claims
// For SELF peer requests skip the signature check (internal traffic). ok, err := claims.GetClaims().DecodeClaimsInToken(host, method, forward, c, publicKey, external)
pp := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), "", "", []string{}, nil).Search(nil, fmt.Sprintf("%v", peer.SELF.EnumIndex()), false)
if len(pp.Data) > 0 {
p := pp.Data[0].(*peer.Peer)
if p.PublicKey == publicKey {
sessionClaims.Session.IDToken["signature"] = ""
}
}
// 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)
if err != nil { if err != nil {
logger.Error().Msg("Failed to decode claims in forward auth: " + err.Error()) fmt.Println("Failed to decode claims", err)
return http.StatusForbidden
} }
if !ok { return ok
return http.StatusForbidden
}
return http.StatusOK
}
// extractBearerToken extracts the token from a "Bearer xxx" Authorization header value
func extractBearerToken(authHeader string) string {
splitToken := strings.Split(authHeader, "Bearer ")
if len(splitToken) < 2 {
return ""
}
return splitToken[1]
} }

View File

@@ -12,7 +12,6 @@ import (
"sync" "sync"
"time" "time"
oclib "cloud.o-forge.io/core/oc-lib"
"github.com/coocood/freecache" "github.com/coocood/freecache"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
"github.com/i-core/rlog" "github.com/i-core/rlog"
@@ -32,9 +31,8 @@ var (
type conn interface { type conn interface {
Bind(bindDN, password string) error Bind(bindDN, password string) error
SearchRoles(attrs ...string) ([]map[string][]string, error) SearchUser(user string, attrs ...string) ([]map[string]interface{}, error)
SearchUser(user string, attrs ...string) ([]map[string][]string, error) SearchUserRoles(user string, attrs ...string) ([]map[string]interface{}, error)
SearchUserRoles(user string, attrs ...string) ([]map[string][]string, error)
Close() error Close() error
} }
@@ -49,7 +47,6 @@ type Config struct {
BindPass string `envconfig:"bindpw" json:"-" desc:"a LDAP bind password"` BindPass string `envconfig:"bindpw" json:"-" desc:"a LDAP bind password"`
BaseDN string `envconfig:"basedn" required:"true" desc:"a LDAP base DN for searching users"` BaseDN string `envconfig:"basedn" required:"true" desc:"a LDAP base DN for searching users"`
AttrClaims map[string]string `envconfig:"attr_claims" default:"name:name,sn:family_name,givenName:given_name,mail:email" desc:"a mapping of LDAP attributes to OpenID connect claims"` AttrClaims map[string]string `envconfig:"attr_claims" default:"name:name,sn:family_name,givenName:given_name,mail:email" desc:"a mapping of LDAP attributes to OpenID connect claims"`
UserBaseDN string `envconfig:"user_basedn" required:"true" desc:"a LDAP base DN for searching users"`
RoleBaseDN string `envconfig:"role_basedn" required:"true" desc:"a LDAP base DN for searching roles"` RoleBaseDN string `envconfig:"role_basedn" required:"true" desc:"a LDAP base DN for searching roles"`
RoleAttr string `envconfig:"role_attr" default:"description" desc:"a LDAP group's attribute that contains a role's name"` RoleAttr string `envconfig:"role_attr" default:"description" desc:"a LDAP group's attribute that contains a role's name"`
RoleClaim string `envconfig:"role_claim" default:"https://github.com/i-core/werther/claims/roles" desc:"a name of an OpenID Connect claim that contains user roles"` RoleClaim string `envconfig:"role_claim" default:"https://github.com/i-core/werther/claims/roles" desc:"a name of an OpenID Connect claim that contains user roles"`
@@ -66,12 +63,11 @@ func New() *Client {
BindDN: conf.GetConfig().LDAPBindDN, BindDN: conf.GetConfig().LDAPBindDN,
BindPass: conf.GetConfig().LDAPBindPW, BindPass: conf.GetConfig().LDAPBindPW,
BaseDN: conf.GetConfig().LDAPBaseDN, BaseDN: conf.GetConfig().LDAPBaseDN,
UserBaseDN: conf.GetConfig().LDAPUserBaseDN,
RoleBaseDN: conf.GetConfig().LDAPRoleBaseDN, RoleBaseDN: conf.GetConfig().LDAPRoleBaseDN,
} }
return &Client{ return &Client{
Config: cnf, Config: cnf,
connector: &ldapConnector{BaseDN: cnf.BaseDN, RoleBaseDN: cnf.RoleBaseDN, UserBaseDN: cnf.UserBaseDN, IsTLS: cnf.IsTLS}, connector: &ldapConnector{BaseDN: cnf.BaseDN, RoleBaseDN: cnf.RoleBaseDN, IsTLS: cnf.IsTLS},
cache: freecache.NewCache(cnf.CacheSize * 1024), cache: freecache.NewCache(cnf.CacheSize * 1024),
} }
} }
@@ -82,29 +78,31 @@ type Client struct {
cache *freecache.Cache cache *freecache.Cache
} }
func (cli *Client) Authenticate(ctx context.Context, username string, password string) (bool, error) { func (cli *Client) Authenticate(ctx context.Context, username, password string) (bool, error) {
if username == "" || password == "" { if username == "" || password == "" {
return false, nil return false, nil
} }
var cancel context.CancelFunc var cancel context.CancelFunc
ctx, cancel = context.WithCancel(ctx) ctx, cancel = context.WithCancel(ctx)
logger := oclib.GetLogger()
logger.Debug().Msgf("LDAP authenticate user: %s", username)
cn, ok := <-cli.connect(ctx) cn, ok := <-cli.connect(ctx)
cancel() cancel()
if !ok { if !ok {
return false, errConnectionTimeout return false, errConnectionTimeout
} }
defer cn.Close() defer cn.Close()
// Find a user DN by his or her username. // Find a user DN by his or her username.
details, err := cli.findBasicUserDetails(cn, username, []string{"dn"}) details, err := cli.findBasicUserDetails(cn, username, []string{"dn"})
if err != nil || details == nil { if err != nil {
return false, err return false, err
} }
a := details["dn"] if details == nil {
logger.Debug().Msgf("Binding DN: %s", a[0]) return false, nil
if err := cn.Bind(a[0], password); err != nil { }
logger.Error().Msg("LDAP bind failed: " + err.Error())
if err := cn.Bind(details["dn"].(string), password); err != nil {
if err == errInvalidCredentials { if err == errInvalidCredentials {
return false, nil return false, nil
} }
@@ -120,21 +118,6 @@ func (cli *Client) Authenticate(ctx context.Context, username string, password s
return true, nil return true, nil
} }
func (cli *Client) GetRoles(ctx context.Context) (map[string]LDAPRoles, error) {
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(ctx)
cn, ok := <-cli.connect(ctx)
cancel()
if !ok {
return map[string]LDAPRoles{}, errConnectionTimeout
}
defer cn.Close()
// Find a user DN by his or her username.
return cli.findRoles(cn, "dn", "member", "uniqueMember")
}
// Claim is the FindOIDCClaims result struct // Claim is the FindOIDCClaims result struct
type LDAPClaim struct { type LDAPClaim struct {
Code string // the root claim name Code string // the root claim name
@@ -142,10 +125,6 @@ type LDAPClaim struct {
Value interface{} // the value Value interface{} // the value
} }
type LDAPRoles struct {
Members map[string][]string
}
// FindOIDCClaims finds all OIDC claims for a user. // FindOIDCClaims finds all OIDC claims for a user.
func (cli *Client) FindOIDCClaims(ctx context.Context, username string) ([]LDAPClaim, error) { func (cli *Client) FindOIDCClaims(ctx context.Context, username string) ([]LDAPClaim, error) {
if username == "" { if username == "" {
@@ -214,12 +193,11 @@ func (cli *Client) FindOIDCClaims(ctx context.Context, username string) ([]LDAPC
roles := make(map[string]interface{}) roles := make(map[string]interface{})
for _, entry := range entries { for _, entry := range entries {
roleDNs, ok := entry["dn"] roleDN, ok := entry["dn"].(string)
if !ok || len(roleDNs) == 0 { if !ok || roleDN == "" {
log.Infow("No required LDAP attribute for a role", "ldapAttribute", "dn", "entry", entry) log.Infow("No required LDAP attribute for a role", "ldapAttribute", "dn", "entry", entry)
continue continue
} }
roleDN := roleDNs[0]
if entry[cli.RoleAttr] == nil { if entry[cli.RoleAttr] == nil {
log.Infow("No required LDAP attribute for a role", "ldapAttribute", cli.RoleAttr, "roleDN", roleDN) log.Infow("No required LDAP attribute for a role", "ldapAttribute", cli.RoleAttr, "roleDN", roleDN)
continue continue
@@ -229,7 +207,7 @@ func (cli *Client) FindOIDCClaims(ctx context.Context, username string) ([]LDAPC
// It's sufficient to compare the DN's suffix with the base DN. // It's sufficient to compare the DN's suffix with the base DN.
n, k := len(roleDN), len(cli.RoleBaseDN) n, k := len(roleDN), len(cli.RoleBaseDN)
if n < k || !strings.EqualFold(roleDN[n-k:], cli.RoleBaseDN) { if n < k || !strings.EqualFold(roleDN[n-k:], cli.RoleBaseDN) {
return nil, errors.New("You should never see that") panic("You should never see that")
} }
// The DN without the role's base DN must contain a CN and OU // The DN without the role's base DN must contain a CN and OU
// where the CN is for uniqueness only, and the OU is an application id. // where the CN is for uniqueness only, and the OU is an application id.
@@ -281,15 +259,13 @@ func (cli *Client) connect(ctx context.Context) <-chan conn {
cn, err := cli.connector.Connect(ctx, addr) cn, err := cli.connector.Connect(ctx, addr)
if err != nil { if err != nil {
log := oclib.GetLogger() fmt.Println("Failed to create a LDAP connection", "address", addr)
log.Error().Msgf("Failed to create LDAP connection to %s: %v", addr, err)
return return
} }
select { select {
case <-ctx.Done(): case <-ctx.Done():
cn.Close() cn.Close()
log := oclib.GetLogger() fmt.Println("a LDAP connection is cancelled", "address", addr)
log.Debug().Msgf("LDAP connection cancelled: %s", addr)
return return
case ch <- cn: case ch <- cn:
} }
@@ -302,102 +278,27 @@ func (cli *Client) connect(ctx context.Context) <-chan conn {
return ch return ch
} }
func (cli *Client) findRoles(cn conn, attrs ...string) (map[string]LDAPRoles, error) {
logger := oclib.GetLogger()
logger.Debug().Msg("Finding LDAP roles")
if cli.BindDN != "" {
// We need to login to a LDAP server with a service account for retrieving user data.
if err := cn.Bind(cli.BindDN, cli.BindPass); err != nil {
return map[string]LDAPRoles{}, errors.New(err.Error() + " : failed to login to a LDAP woth a service account")
}
}
entries, err := cn.SearchRoles(attrs...)
logger.Debug().Msgf("Found %d LDAP role entries", len(entries))
if err != nil {
return map[string]LDAPRoles{}, err
}
claims := map[string]LDAPRoles{}
for _, entry := range entries {
roleDNs, ok := entry["dn"]
if !ok || len(roleDNs) == 0 {
continue
}
roleDN := roleDNs[0]
// Ensure that a role's DN is inside of the role's base DN.
// It's sufficient to compare the DN's suffix with the base DN.
n, k := len(roleDN), len(cli.RoleBaseDN)
if n < k || !strings.EqualFold(roleDN[n-k:], cli.RoleBaseDN) {
return nil, errors.New("You should never see that")
}
// The DN without the role's base DN must contain a CN and OU
// where the CN is for uniqueness only, and the OU is an application id.
path := strings.Split(roleDN[:n-k-1], ",")
if len(path) != 2 {
continue
}
appID := path[1][len("OU="):]
if _, ok := claims[appID]; !ok {
claims[appID] = LDAPRoles{
Members: map[string][]string{},
}
}
role := path[0][len("cn="):]
if claims[appID].Members[role] == nil {
claims[appID].Members[role] = []string{}
}
logger.Debug().Msgf("Processing role entry: %v", entry["dn"])
memberDNs, ok := entry["member"]
for _, memberDN := range memberDNs {
if !ok || memberDN == "" {
continue
}
path = strings.Split(memberDN[:n-k-1], ",")
if len(path) < 1 {
continue
}
member := strings.Split(path[0][len("uid="):], ",")
claims[appID].Members[role] = append(claims[appID].Members[role], member[0])
}
memberDNs, ok = entry["uniqueMember"]
for _, memberDN := range memberDNs {
if !ok || memberDN == "" {
continue
}
path = strings.Split(memberDN[:n-k-1], ",")
if len(path) < 1 {
continue
}
member := strings.Split(path[0][len("uid="):], ",")
claims[appID].Members[role] = append(claims[appID].Members[role], member[0])
}
}
return claims, nil
}
// findBasicUserDetails finds user's LDAP attributes that were specified. It returns nil if no such user. // findBasicUserDetails finds user's LDAP attributes that were specified. It returns nil if no such user.
func (cli *Client) findBasicUserDetails(cn conn, username string, attrs []string) (map[string][]string, error) { func (cli *Client) findBasicUserDetails(cn conn, username string, attrs []string) (map[string]interface{}, error) {
logger := oclib.GetLogger()
logger.Debug().Msgf("Finding LDAP user details for: %s", username)
if cli.BindDN != "" { if cli.BindDN != "" {
// We need to login to a LDAP server with a service account for retrieving user data. // We need to login to a LDAP server with a service account for retrieving user data.
if err := cn.Bind(cli.BindDN, cli.BindPass); err != nil { if err := cn.Bind(cli.BindDN, cli.BindPass); err != nil {
return nil, errors.New(err.Error() + " : failed to login to a LDAP woth a service account") return nil, errors.New(err.Error() + " : failed to login to a LDAP woth a service account")
} }
} }
entries, err := cn.SearchUser(username, attrs...) entries, err := cn.SearchUser(username, attrs...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(entries) == 0 { if len(entries) != 1 {
// We didn't find the user. // We didn't find the user.
logger.Debug().Msgf("LDAP user not found: %s", username)
return nil, nil return nil, nil
} }
var ( var (
entry = entries[0] entry = entries[0]
details = make(map[string][]string) details = make(map[string]interface{})
) )
for _, attr := range attrs { for _, attr := range attrs {
if v, ok := entry[attr]; ok { if v, ok := entry[attr]; ok {
@@ -410,7 +311,6 @@ func (cli *Client) findBasicUserDetails(cn conn, username string, attrs []string
type ldapConnector struct { type ldapConnector struct {
BaseDN string BaseDN string
RoleBaseDN string RoleBaseDN string
UserBaseDN string
IsTLS bool IsTLS bool
} }
@@ -432,13 +332,12 @@ func (c *ldapConnector) Connect(ctx context.Context, addr string) (conn, error)
ldapcn := ldap.NewConn(tcpcn, c.IsTLS) ldapcn := ldap.NewConn(tcpcn, c.IsTLS)
ldapcn.Start() ldapcn.Start()
return &ldapConn{Conn: ldapcn, BaseDN: c.BaseDN, UserBaseDN: c.UserBaseDN, RoleBaseDN: c.RoleBaseDN}, nil return &ldapConn{Conn: ldapcn, BaseDN: c.BaseDN, RoleBaseDN: c.RoleBaseDN}, nil
} }
type ldapConn struct { type ldapConn struct {
*ldap.Conn *ldap.Conn
BaseDN string BaseDN string
UserBaseDN string
RoleBaseDN string RoleBaseDN string
} }
@@ -450,43 +349,35 @@ func (c *ldapConn) Bind(bindDN, password string) error {
return err return err
} }
func (c *ldapConn) SearchUser(user string, attrs ...string) ([]map[string][]string, error) { func (c *ldapConn) SearchUser(user string, attrs ...string) ([]map[string]interface{}, error) {
query := fmt.Sprintf( query := fmt.Sprintf(
"(&(|(objectClass=organizationalPerson)(objectClass=inetOrgPerson))"+ "(&(|(objectClass=organizationalPerson)(objectClass=inetOrgPerson))"+
"(|(uid=%[1]s)(mail=%[1]s)(userPrincipalName=%[1]s)(sAMAccountName=%[1]s)))", user) "(|(uid=%[1]s)(mail=%[1]s)(userPrincipalName=%[1]s)(sAMAccountName=%[1]s)))", user)
return c.searchEntries(c.UserBaseDN, query, attrs) return c.searchEntries(c.BaseDN, query, attrs)
} }
func (c *ldapConn) SearchUserRoles(user string, attrs ...string) ([]map[string][]string, error) { func (c *ldapConn) SearchUserRoles(user string, attrs ...string) ([]map[string]interface{}, error) {
query := fmt.Sprintf("(|"+ query := fmt.Sprintf("(|"+
"(&(|(objectClass=group)(objectClass=groupOfNames)(objectClass=groupofnames))(member=%[1]s))"+ "(&(|(objectClass=group)(objectClass=groupOfNames))(member=%[1]s))"+
"(&(objectClass=groupOfUniqueNames)(uniqueMember=%[1]s))"+ "(&(objectClass=groupOfUniqueNames)(uniqueMember=%[1]s))"+
")", user) ")", user)
return c.searchEntries(c.RoleBaseDN, query, attrs) return c.searchEntries(c.RoleBaseDN, query, attrs)
} }
func (c *ldapConn) SearchRoles(attrs ...string) ([]map[string][]string, error) {
query := "(|(&(|(objectClass=group)(objectClass=groupOfNames)(objectClass=groupofnames))))"
return c.searchEntries(c.RoleBaseDN, query, attrs)
}
// searchEntries executes a LDAP query, and returns a result as entries where each entry is mapping of LDAP attributes. // searchEntries executes a LDAP query, and returns a result as entries where each entry is mapping of LDAP attributes.
func (c *ldapConn) searchEntries(baseDN, query string, attrs []string) ([]map[string][]string, error) { func (c *ldapConn) searchEntries(baseDN, query string, attrs []string) ([]map[string]interface{}, error) {
log := oclib.GetLogger()
log.Debug().Msgf("LDAP search: baseDN=%s query=%s", baseDN, query)
req := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, query, attrs, nil) req := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, query, attrs, nil)
res, err := c.Search(req) res, err := c.Search(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
log.Debug().Msgf("LDAP search returned %d entries", len(res.Entries))
var entries []map[string][]string var entries []map[string]interface{}
for _, v := range res.Entries { for _, v := range res.Entries {
entry := map[string][]string{"dn": {v.DN}} entry := map[string]interface{}{"dn": v.DN}
for _, attr := range v.Attributes { for _, attr := range v.Attributes {
// We need the first value only for the named attribute. // We need the first value only for the named attribute.
entry[attr.Name] = attr.Values entry[attr.Name] = attr.Values[0]
} }
entries = append(entries, entry) entries = append(entries, entry)
} }

View File

@@ -2,28 +2,23 @@ package claims
import ( import (
"oc-auth/conf" "oc-auth/conf"
"strings"
"cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/models/peer"
) )
// ClaimService builds and verifies OAuth2 session claims // Tokenizer interface
type ClaimService interface { type ClaimService interface {
// BuildConsentSession builds the session payload for Hydra consent accept. AddClaimsToToken(userId string, peer *peer.Peer) Claims
// Claims are injected into the Hydra JWT via the consent session, not appended to the token.
BuildConsentSession(clientID string, userId string, peer *peer.Peer) Claims
// DecodeClaimsInToken verifies permissions from claims extracted from a JWT
DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, error) DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, error)
} }
// SessionClaims contains access_token and id_token claim maps // SessionClaims struct
type SessionClaims struct { type SessionClaims struct {
AccessToken map[string]interface{} `json:"access_token"` AccessToken map[string]interface{} `json:"access_token"`
IDToken map[string]interface{} `json:"id_token"` IDToken map[string]interface{} `json:"id_token"`
} }
// Claims is the top-level session structure passed to Hydra consent accept // Claims struct
type Claims struct { type Claims struct {
Session SessionClaims `json:"session"` Session SessionClaims `json:"session"`
} }
@@ -33,10 +28,5 @@ var t = map[string]ClaimService{
} }
func GetClaims() ClaimService { func GetClaims() ClaimService {
for k := range t { return t[conf.GetConfig().Auth]
if strings.Contains(conf.GetConfig().Auth, k) {
return t[k]
}
}
return nil
} }

View File

@@ -9,8 +9,8 @@ import (
"oc-auth/infrastructure/utils" "oc-auth/infrastructure/utils"
"os" "os"
"strings" "strings"
"time"
oclib "cloud.o-forge.io/core/oc-lib"
"cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
@@ -26,7 +26,7 @@ func (h HydraClaims) generateKey(relation string, path string) (string, error) {
return strings.ToUpper(method.String()) + "_" + strings.ReplaceAll(p, ":", ""), nil return strings.ToUpper(method.String()) + "_" + strings.ReplaceAll(p, ":", ""), nil
} }
// decodeKey extracts method and path from a permission key // decode key expect to extract method and path from key
func (h HydraClaims) decodeKey(key string, external bool) (tools.METHOD, string, error) { func (h HydraClaims) decodeKey(key string, external bool) (tools.METHOD, string, error) {
s := strings.Split(key, "_") s := strings.Split(key, "_")
if len(s) < 2 { if len(s) < 2 {
@@ -45,10 +45,7 @@ func (h HydraClaims) decodeKey(key string, external bool) (tools.METHOD, string,
func (h HydraClaims) DecodeSignature(host string, signature string, publicKey string) (bool, error) { func (h HydraClaims) DecodeSignature(host string, signature string, publicKey string) (bool, error) {
hashed := sha256.Sum256([]byte(host)) hashed := sha256.Sum256([]byte(host))
spkiBlock, _ := pem.Decode([]byte(publicKey)) spkiBlock, _ := pem.Decode([]byte(publicKey)) // get public key into a variable
if spkiBlock == nil {
return false, errors.New("failed to decode public key PEM")
}
err := VerifyDefault(hashed[:], spkiBlock.Bytes, signature) err := VerifyDefault(hashed[:], spkiBlock.Bytes, signature)
if err != nil { if err != nil {
return false, err return false, err
@@ -58,19 +55,18 @@ func (h HydraClaims) DecodeSignature(host string, signature string, publicKey st
func (h HydraClaims) encodeSignature(host string) (string, error) { func (h HydraClaims) encodeSignature(host string) (string, error) {
hashed := sha256.Sum256([]byte(host)) hashed := sha256.Sum256([]byte(host))
// READ FILE TO GET PRIVATE KEY FROM PVK PEM PATH
content, err := os.ReadFile(conf.GetConfig().PrivateKeyPath) content, err := os.ReadFile(conf.GetConfig().PrivateKeyPath)
if err != nil { if err != nil {
return "", err return "", err
} }
privateKey := string(content) privateKey := string(content)
spkiBlock, _ := pem.Decode([]byte(privateKey)) spkiBlock, _ := pem.Decode([]byte(privateKey))
if spkiBlock == nil {
return "", errors.New("failed to decode private key PEM")
}
return SignDefault(hashed[:], spkiBlock.Bytes) return SignDefault(hashed[:], spkiBlock.Bytes)
} }
func (h HydraClaims) clearBlank(path []string) []string { func (h HydraClaims) clearBlank(path []string) []string {
// clear blank
newPath := []string{} newPath := []string{}
for _, p := range path { for _, p := range path {
if p != "" { if p != "" {
@@ -80,33 +76,29 @@ func (h HydraClaims) clearBlank(path []string) []string {
return newPath return newPath
} }
// DecodeClaimsInToken verifies permissions from claims in a standard JWT (via introspection) func (a HydraClaims) CheckExpiry(exp int64) bool {
now := time.Now().UTC().Unix()
return now <= exp
}
func (h HydraClaims) DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, error) { func (h HydraClaims) DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, error) {
logger := oclib.GetLogger()
idTokenClaims := sessionClaims.Session.IDToken idTokenClaims := sessionClaims.Session.IDToken
if idTokenClaims["signature"] == nil {
// Signature verification: skip if signature is empty (internal requests) return false, errors.New("no signature found")
if sig, ok := idTokenClaims["signature"].(string); ok && sig != "" { }
if ok, err := h.DecodeSignature(host, sig, publicKey); !ok { signature := idTokenClaims["signature"].(string)
return false, err if ok, err := h.DecodeSignature(host, signature, publicKey); !ok {
} return false, err
} }
claims := sessionClaims.Session.AccessToken claims := sessionClaims.Session.AccessToken
if claims == nil {
return false, errors.New("no access_token claims found")
}
path := strings.ReplaceAll(forward, "http://"+host, "") path := strings.ReplaceAll(forward, "http://"+host, "")
splittedPath := h.clearBlank(strings.Split(path, "/")) splittedPath := h.clearBlank(strings.Split(path, "/"))
if _, ok := claims["exp"].(float64); !ok || !h.CheckExpiry(int64(claims["exp"].(float64))) {
return false, errors.New("token is expired")
}
for m, p := range claims { for m, p := range claims {
pStr, ok := p.(string)
if !ok {
continue
}
match := true match := true
splittedP := h.clearBlank(strings.Split(pStr, "/")) splittedP := h.clearBlank(strings.Split(p.(string), "/"))
if len(splittedP) != len(splittedPath) { if len(splittedP) != len(splittedPath) {
continue continue
} }
@@ -125,53 +117,43 @@ func (h HydraClaims) DecodeClaimsInToken(host string, method string, forward str
} }
perm := perms_connectors.Permission{ perm := perms_connectors.Permission{
Relation: "permits" + strings.ToUpper(meth.String()), Relation: "permits" + strings.ToUpper(meth.String()),
Object: pStr, Object: p.(string),
} }
return perms_connectors.GetPermissionConnector("").CheckPermission(perm, nil, true), nil return perms_connectors.GetPermissionConnector().CheckPermission(perm, nil, true), nil
} }
} }
logger.Error().Msg("No permission found for " + method + " " + forward)
return false, errors.New("no permission found") return false, errors.New("no permission found")
} }
// BuildConsentSession builds the session payload for Hydra consent accept. // add claims to token method of HydraTokenizer
// Claims are injected into the Hydra JWT — not appended to the token as before. func (h HydraClaims) AddClaimsToToken(userId string, p *peer.Peer) Claims {
func (h HydraClaims) BuildConsentSession(clientID string, userId string, p *peer.Peer) Claims { claims := Claims{}
logger := oclib.GetLogger()
c := Claims{}
perms, err := perms_connectors.KetoConnector{}.GetPermissionByUser(userId, true) perms, err := perms_connectors.KetoConnector{}.GetPermissionByUser(userId, true)
if err != nil { if err != nil {
logger.Error().Msg("Failed to get permissions for user " + userId + ": " + err.Error()) return claims
return c
} }
claims.Session.AccessToken = make(map[string]interface{})
c.Session.AccessToken = make(map[string]interface{}) claims.Session.IDToken = make(map[string]interface{})
c.Session.IDToken = make(map[string]interface{})
for _, perm := range perms { for _, perm := range perms {
key, err := h.generateKey(strings.ReplaceAll(perm.Relation, "permits", ""), perm.Subject) key, err := h.generateKey(strings.ReplaceAll(perm.Relation, "permits", ""), perm.Subject)
if err != nil { if err != nil {
continue continue
} }
c.Session.AccessToken[key] = perm.Subject claims.Session.AccessToken[key] = perm.Subject
} }
sign, err := h.encodeSignature(p.Url)
sign, err := h.encodeSignature(p.APIUrl)
if err != nil { if err != nil {
logger.Error().Msg("Failed to encode signature: " + err.Error()) return claims
return c
} }
claims.Session.IDToken["peer_id"] = p.UUID
c.Session.IDToken["username"] = userId // we should get group from user
c.Session.IDToken["peer_id"] = p.UUID
c.Session.IDToken["client_id"] = clientID
groups, err := perms_connectors.KetoConnector{}.GetGroupByUser(userId) groups, err := perms_connectors.KetoConnector{}.GetGroupByUser(userId)
if err != nil { if err != nil {
logger.Error().Msg("Failed to get groups for user " + userId + ": " + err.Error()) return claims
return c
} }
c.Session.IDToken["groups"] = groups claims.Session.IDToken["groups"] = groups
c.Session.IDToken["signature"] = sign claims.Session.IDToken["signature"] = sign
return c return claims
} }
// add signature in the token MISSING

View File

@@ -10,8 +10,8 @@ func GetAuthConnector() auth_connectors.AuthConnector {
return auth_connectors.GetAuthConnector() return auth_connectors.GetAuthConnector()
} }
func GetPermissionConnector(client string) perms_connectors.PermConnector { func GetPermissionConnector() perms_connectors.PermConnector {
return perms_connectors.GetPermissionConnector(client) return perms_connectors.GetPermissionConnector()
} }
func GetClaims() claims.ClaimService { func GetClaims() claims.ClaimService {

View File

@@ -6,29 +6,24 @@ import (
"fmt" "fmt"
"oc-auth/conf" "oc-auth/conf"
"oc-auth/infrastructure/utils" "oc-auth/infrastructure/utils"
"strings"
oclib "cloud.o-forge.io/core/oc-lib" oclib "cloud.o-forge.io/core/oc-lib"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
type KetoConnector struct { type KetoConnector struct{}
Client string
}
func (k KetoConnector) SetClient(client string) {
k.Client = client
}
func (k KetoConnector) namespace() string { func (k KetoConnector) namespace() string {
return "open-cloud" return "open-cloud"
} }
func (k KetoConnector) scope() string { func (k KetoConnector) scope() string {
return "oc-auth-realm" return "oc-auth"
} }
func (f KetoConnector) permToQuery(perm Permission, permDependancies *Permission) string { func (f KetoConnector) permToQuery(perm Permission, permDependancies *Permission) string {
n := "?namespace=" + f.namespace() n := "?namespace=" + perm.Namespace()
if perm.Object != "" { if perm.Object != "" {
n += "&object=" + perm.Object n += "&object=" + perm.Object
} }
@@ -56,10 +51,7 @@ func (f KetoConnector) permToQuery(perm Permission, permDependancies *Permission
func (k KetoConnector) Status() tools.State { func (k KetoConnector) Status() tools.State {
caller := tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{}) caller := tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{})
var responseBody map[string]interface{} var responseBody map[string]interface{}
host := conf.GetConfig().PermissionConnectorReadHost host := conf.GetConfig().PermissionConnectorHost
if conf.GetConfig().Local {
host = "localhost"
}
port := fmt.Sprintf("%v", conf.GetConfig().PermissionConnectorPort) port := fmt.Sprintf("%v", conf.GetConfig().PermissionConnectorPort)
resp, err := caller.CallGet("http://"+host+":"+port, "/health/ready") resp, err := caller.CallGet("http://"+host+":"+port, "/health/ready")
if err != nil { if err != nil {
@@ -81,7 +73,7 @@ func (k KetoConnector) CheckPermission(perm Permission, permDependancies *Permis
perms, err := k.GetPermission(perm.Object, perm.Relation) perms, err := k.GetPermission(perm.Object, perm.Relation)
if err != nil { if err != nil {
log := oclib.GetLogger() log := oclib.GetLogger()
log.Error().Msg("CheckPermission " + err.Error()) log.Error().Msg(err.Error())
return false return false
} }
return len(perms) > 0 return len(perms) > 0
@@ -197,8 +189,6 @@ func (k KetoConnector) GetPermissionByRole(roleID string) ([]Permission, error)
} }
func (k KetoConnector) GetPermissionByUser(userID string, internal bool) ([]Permission, error) { func (k KetoConnector) GetPermissionByUser(userID string, internal bool) ([]Permission, error) {
roles, err := k.get("", "member", userID) roles, err := k.get("", "member", userID)
log := oclib.GetLogger()
log.Debug().Msgf("GetPermissionByUser roles for %s: %d roles, err=%v", userID, len(roles), err)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -221,10 +211,7 @@ func (k KetoConnector) GetPermissionByUser(userID string, internal bool) ([]Perm
func (k KetoConnector) get(object string, relation string, subject string) ([]Permission, error) { func (k KetoConnector) get(object string, relation string, subject string) ([]Permission, error) {
t := []Permission{} t := []Permission{}
caller := tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{}) caller := tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{})
host := conf.GetConfig().PermissionConnectorReadHost host := conf.GetConfig().PermissionConnectorHost
if conf.GetConfig().Local {
host = "localhost"
}
port := fmt.Sprintf("%v", conf.GetConfig().PermissionConnectorPort) port := fmt.Sprintf("%v", conf.GetConfig().PermissionConnectorPort)
resp, err := caller.CallGet("http://"+host+":"+port, "/relation-tuples"+k.permToQuery( resp, err := caller.CallGet("http://"+host+":"+port, "/relation-tuples"+k.permToQuery(
Permission{Object: object, Relation: relation, Subject: subject}, nil)) Permission{Object: object, Relation: relation, Subject: subject}, nil))
@@ -248,7 +235,7 @@ func (k KetoConnector) get(object string, relation string, subject string) ([]Pe
return t, nil return t, nil
} }
func (k KetoConnector) binds(object string, relation string, subject string) (string, int, error) { func (k KetoConnector) binds(subject string, relation string, object string) (string, int, error) {
_, code, err := k.createRelationShip(object, relation, subject, nil) _, code, err := k.createRelationShip(object, relation, subject, nil)
if err != nil { if err != nil {
return object, code, err return object, code, err
@@ -257,8 +244,6 @@ func (k KetoConnector) binds(object string, relation string, subject string) (st
} }
func (k KetoConnector) BindRole(userID string, roleID string) (string, int, error) { func (k KetoConnector) BindRole(userID string, roleID string) (string, int, error) {
log := oclib.GetLogger()
log.Debug().Msgf("BindRole: user=%s role=%s", userID, roleID)
return k.binds(userID, "member", roleID) return k.binds(userID, "member", roleID)
} }
@@ -339,6 +324,9 @@ func (k KetoConnector) UnBindPermission(roleID string, permID string, relation s
} }
func (k KetoConnector) createRelationShip(object string, relation string, subject string, subPerm *Permission) (*Permission, int, error) { func (k KetoConnector) createRelationShip(object string, relation string, subject string, subPerm *Permission) (*Permission, int, error) {
exist, err := k.get(object, relation, subject) exist, err := k.get(object, relation, subject)
if strings.Contains(subject, "/workflow/:id") {
fmt.Println("subject", subject, relation, exist, err)
}
if err == nil && len(exist) > 0 { if err == nil && len(exist) > 0 {
return nil, 409, errors.New("Relation already exist") return nil, 409, errors.New("Relation already exist")
} }
@@ -350,40 +338,34 @@ func (k KetoConnector) createRelationShip(object string, relation string, subjec
if err != nil { if err != nil {
return nil, code, err return nil, code, err
} }
body["subject_set"] = map[string]interface{}{"namespace": k.namespace(), "object": s.Object, "relation": s.Relation, "subject_id": s.Subject} body["subject_set"] = map[string]interface{}{"namespace": s.Namespace(), "object": s.Object, "relation": s.Relation, "subject_id": s.Subject}
}
host := conf.GetConfig().PermissionConnectorWriteHost
if conf.GetConfig().Local {
host = "localhost"
} }
host := conf.GetConfig().PermissionConnectorHost
port := fmt.Sprintf("%v", conf.GetConfig().PermissionConnectorAdminPort) port := fmt.Sprintf("%v", conf.GetConfig().PermissionConnectorAdminPort)
b, err := caller.CallPut("http://"+host+":"+port, "/relation-tuples", body) b, err := caller.CallPut("http://"+host+":"+port, "/relation-tuples", body)
if err != nil { if err != nil {
log := oclib.GetLogger() log := oclib.GetLogger()
log.Error().Msg("createRelationShip" + err.Error()) log.Error().Msg(err.Error())
return nil, 500, err return nil, 500, err
} }
data := map[string]interface{}{} var data map[string]interface{}
err = json.Unmarshal(b, &data) err = json.Unmarshal(b, &data)
if err != nil { if err != nil {
log := oclib.GetLogger() log := oclib.GetLogger()
log.Error().Msgf("createRelationShip unmarshal error: %s, err=%v", string(b), err) log.Error().Msg(err.Error())
return nil, 500, err return nil, 500, err
} }
perm := &Permission{} perm := &Permission{
if data != nil { Object: data["object"].(string),
perm = &Permission{ Relation: data["relation"].(string),
Object: data["object"].(string), Subject: data["subject_id"].(string),
Relation: data["relation"].(string), }
Subject: data["subject_id"].(string), if data["subject_set"] != nil {
} sub := data["subject_set"].(map[string]interface{})
if data["subject_set"] != nil { perm.SubPermission = &Permission{
sub := data["subject_set"].(map[string]interface{}) Object: sub["object"].(string),
perm.SubPermission = &Permission{ Relation: sub["relation"].(string),
Object: sub["object"].(string), Subject: sub["subject_id"].(string),
Relation: sub["relation"].(string),
Subject: sub["subject_id"].(string),
}
} }
} }
return perm, 200, nil return perm, 200, nil
@@ -396,15 +378,12 @@ func (k KetoConnector) deleteRelationShip(object string, relation string, subjec
} }
caller := tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{}) caller := tools.NewHTTPCaller(map[tools.DataType]map[tools.METHOD]string{})
n := k.permToQuery(Permission{Object: object, Relation: relation, Subject: subject}, subPerm) n := k.permToQuery(Permission{Object: object, Relation: relation, Subject: subject}, subPerm)
host := conf.GetConfig().PermissionConnectorWriteHost host := conf.GetConfig().PermissionConnectorHost
if conf.GetConfig().Local {
host = "localhost"
}
port := fmt.Sprintf("%v", conf.GetConfig().PermissionConnectorAdminPort) port := fmt.Sprintf("%v", conf.GetConfig().PermissionConnectorAdminPort)
b, err := caller.CallDelete("http://"+host+":"+port, "/relation-tuples"+n) b, err := caller.CallDelete("http://"+host+":"+port, "/relation-tuples"+n)
if err != nil { if err != nil {
log := oclib.GetLogger() log := oclib.GetLogger()
log.Error().Msg("deleteRelationShip " + err.Error()) log.Error().Msg(err.Error())
return nil, 500, err return nil, 500, err
} }
var data map[string]interface{} var data map[string]interface{}

View File

@@ -1,9 +1,6 @@
package perms_connectors package perms_connectors
import ( import (
"oc-auth/conf"
"strings"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
@@ -24,7 +21,6 @@ func (k Permission) Scope() string {
type PermConnector interface { type PermConnector interface {
Status() tools.State Status() tools.State
SetClient(scope string)
CheckPermission(perm Permission, permDependancies *Permission, internal bool) bool CheckPermission(perm Permission, permDependancies *Permission, internal bool) bool
BindRole(userID string, roleID string) (string, int, error) BindRole(userID string, roleID string) (string, int, error)
BindGroup(userID string, groupID string) (string, int, error) BindGroup(userID string, groupID string) (string, int, error)
@@ -55,11 +51,6 @@ var c = map[string]PermConnector{
"keto": KetoConnector{}, "keto": KetoConnector{},
} }
func GetPermissionConnector(scope string) PermConnector { func GetPermissionConnector() PermConnector {
for k := range c { return c["keto"]
if strings.Contains(conf.GetConfig().PermissionConnectorReadHost, k) {
return c[k]
}
}
return nil
} }

21
keto/docker-compose.yml Normal file
View File

@@ -0,0 +1,21 @@
version: '3.4'
services:
keto:
image: oryd/keto:v0.7.0-alpha.1-sqlite
ports:
- "4466:4466"
- "4467:4467"
command: serve -c /home/ory/keto.yml
restart: on-failure
volumes:
- type: bind
source: .
target: /home/ory
container_name: keto
networks:
- catalog
networks:
catalog:
external: true

18
keto/keto.yml Normal file
View File

@@ -0,0 +1,18 @@
version: v0.6.0-alpha.1
log:
level: debug
namespaces:
- id: 0
name: open-cloud
dsn: memory
serve:
read:
host: 0.0.0.0
port: 4466
write:
host: 0.0.0.0
port: 4467

View File

@@ -0,0 +1,79 @@
version: "3"
services:
hydra-client:
image: oryd/hydra:v2.2.0
container_name: hydra-client
environment:
HYDRA_ADMIN_URL: http://hydra:4445
ORY_SDK_URL: http://hydra:4445
command:
- create
- oauth2-client
- --skip-tls-verify
- --name
- test-client
- --secret
- oc-auth-got-secret
- --response-type
- id_token,token,code
- --grant-type
- implicit,refresh_token,authorization_code,client_credentials
- --scope
- openid,profile,email,roles
- --token-endpoint-auth-method
- client_secret_post
- --redirect-uri
- http://localhost:3000
networks:
- hydra-net
- catalog
deploy:
restart_policy:
condition: none
depends_on:
- hydra
healthcheck:
test: ["CMD", "curl", "-f", "http://hydra:4445"]
interval: 10s
timeout: 10s
retries: 10
hydra:
container_name: hydra
image: oryd/hydra:v2.2.0
environment:
SECRETS_SYSTEM: oc-auth-got-secret
LOG_LEAK_SENSITIVE_VALUES: true
# OAUTH2_TOKEN_HOOK_URL: http://oc-auth:8080/oc/claims
URLS_SELF_ISSUER: http://hydra:4444
URLS_SELF_PUBLIC: http://hydra:4444
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_SCOPES: profile,email,phone,roles
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_CLAIMS: name,family_name,given_name,nickname,email,phone_number
DSN: memory
command: serve all --dev
networks:
- hydra-net
- catalog
ports:
- "4444:4444"
- "4445:4445"
deploy:
restart_policy:
condition: on-failure
ldap:
image: pgarrett/ldap-alpine
container_name: ldap
volumes:
- "./ldap.ldif:/ldif/ldap.ldif"
networks:
- hydra-net
- catalog
ports:
- "390:389"
deploy:
restart_policy:
condition: on-failure
networks:
hydra-net:
catalog:
external: true

24
ldap-hydra/ldap.ldif Normal file
View File

@@ -0,0 +1,24 @@
dn: uid=admin,ou=Users,dc=example,dc=com
objectClass: inetOrgPerson
cn: Admin
sn: Istrator
uid: admin
userPassword: admin
mail: admin@example.com
ou: Users
dn: ou=AppRoles,dc=example,dc=com
objectClass: organizationalunit
ou: AppRoles
description: AppRoles
dn: ou=App1,ou=AppRoles,dc=example,dc=com
objectClass: organizationalunit
ou: App1
description: App1
dn: cn=traveler,ou=App1,ou=AppRoles,dc=example,dc=com
objectClass: groupofnames
cn: traveler
description: traveler
member: uid=admin,ou=Users,dc=example,dc=com

168
main.go
View File

@@ -1,20 +1,22 @@
package main package main
import ( import (
"context" "errors"
"encoding/json"
"oc-auth/conf" "oc-auth/conf"
"oc-auth/infrastructure" "oc-auth/infrastructure"
auth_connectors "oc-auth/infrastructure/auth_connector"
_ "oc-auth/routers" _ "oc-auth/routers"
"os"
"strconv"
"strings" "strings"
"time"
oclib "cloud.o-forge.io/core/oc-lib" oclib "cloud.o-forge.io/core/oc-lib"
peer "cloud.o-forge.io/core/oc-lib/models/peer"
"cloud.o-forge.io/core/oc-lib/models/utils"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
beego "github.com/beego/beego/v2/server/web" beego "github.com/beego/beego/v2/server/web"
) )
const test_name = "test-pierre"
const appname = "oc-auth" const appname = "oc-auth"
// @securityDefinitions.apikey Bearer // @securityDefinitions.apikey Bearer
@@ -22,120 +24,96 @@ const appname = "oc-auth"
// @name Authorization // @name Authorization
// @description Type "Bearer" followed by a space and JWT token. // @description Type "Bearer" followed by a space and JWT token.
func main() { func main() {
oclib.InitAPI(appname) // Init the oc-lib
oclib.Init(appname)
// Load the right config file // Load the right config file
o := oclib.GetConfLoader(appname) o := oclib.GetConfLoader()
conf.GetConfig().AdminRole = o.GetStringDefault("ADMIN_ROLE", "admin") conf.GetConfig().AdminRole = o.GetStringDefault("ADMIN_ROLE", "admin")
conf.GetConfig().PublicKeyPath = o.GetStringDefault("PUBLIC_KEY_PATH", "./pem/public.pem") conf.GetConfig().PublicKeyPath = o.GetStringDefault("PUBLIC_KEY_PATH", "./pem/public.pem")
conf.GetConfig().PrivateKeyPath = o.GetStringDefault("PRIVATE_KEY_PATH", "./pem/private.pem") conf.GetConfig().PrivateKeyPath = o.GetStringDefault("PRIVATE_KEY_PATH", "./pem/private.pem")
conf.GetConfig().ClientSecret = o.GetStringDefault("CLIENT_SECRET", "oc-auth-got-secret") conf.GetConfig().ClientSecret = o.GetStringDefault("CLIENT_SECRET", "oc-auth-got-secret")
conf.GetConfig().OAuth2ClientSecretName = o.GetStringDefault("OAUTH2_CLIENT_SECRET_NAME", "oc-oauth2-client-secret")
conf.GetConfig().OAuth2ClientSecretNamespace = o.GetStringDefault("NAMESPACE", "default")
conf.GetConfig().Auth = o.GetStringDefault("AUTH", "hydra") conf.GetConfig().Auth = o.GetStringDefault("AUTH", "hydra")
conf.GetConfig().AuthConnectorHost = o.GetStringDefault("AUTH_CONNECTOR_HOST", "localhost") conf.GetConfig().AuthConnectorHost = o.GetStringDefault("AUTH_CONNECTOR_HOST", "localhost")
conf.GetConfig().AuthConnectPublicHost = o.GetStringDefault("AUTH_CONNECTOR_PUBLIC_HOST", "localhost")
conf.GetConfig().AuthConnectorPort = o.GetIntDefault("AUTH_CONNECTOR_PORT", 4444) conf.GetConfig().AuthConnectorPort = o.GetIntDefault("AUTH_CONNECTOR_PORT", 4444)
conf.GetConfig().AuthConnectorAdminPort = o.GetStringDefault("AUTH_CONNECTOR_ADMIN_PORT", "4445/admin") conf.GetConfig().AuthConnectorAdminPort = o.GetIntDefault("AUTH_CONNECTOR_ADMIN_PORT", 4445)
conf.GetConfig().PermissionConnectorWriteHost = o.GetStringDefault("PERMISSION_CONNECTOR_WRITE_HOST", "keto") conf.GetConfig().PermissionConnectorHost = o.GetStringDefault("PERMISSION_CONNECTOR_HOST", "keto")
conf.GetConfig().PermissionConnectorReadHost = o.GetStringDefault("PERMISSION_CONNECTOR_READ_HOST", "keto") conf.GetConfig().PermissionConnectorPort = o.GetIntDefault("PERMISSION_CONNECTOR_PORT", 4466)
conf.GetConfig().PermissionConnectorPort = o.GetStringDefault("PERMISSION_CONNECTOR_PORT", "4466") conf.GetConfig().PermissionConnectorAdminPort = o.GetIntDefault("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") // config LDAP
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)
// config LDAPauth
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")
conf.GetConfig().LDAPBindPW = o.GetStringDefault("LDAP_BINDPW", "password") conf.GetConfig().LDAPBindPW = o.GetStringDefault("LDAP_BINDPW", "password")
conf.GetConfig().LDAPBaseDN = o.GetStringDefault("LDAP_BASEDN", "dc=example,dc=com") conf.GetConfig().LDAPBaseDN = o.GetStringDefault("LDAP_BASEDN", "dc=example,dc=com")
conf.GetConfig().LDAPUserBaseDN = o.GetStringDefault("LDAP_USER_BASEDN", "ou=users,dc=example,dc=com")
conf.GetConfig().LDAPRoleBaseDN = o.GetStringDefault("LDAP_ROLE_BASEDN", "ou=AppRoles,dc=example,dc=com") conf.GetConfig().LDAPRoleBaseDN = o.GetStringDefault("LDAP_ROLE_BASEDN", "ou=AppRoles,dc=example,dc=com")
go generateRole() err := generateSelfPeer()
go discovery() if err != nil {
panic(err)
}
discovery()
beego.Run() beego.Run()
} }
func generateRole() { func generateSelfPeer() error {
logger := oclib.GetLogger() requester := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), test_name, "1234", nil, nil)
defer func() {
if r := recover(); r != nil { // TODO check if files at private & public path are set
logger.Error().Msgf("generateRole recovered from panic: %v", r) // check if files at private & public path are set
} if _, err := os.Stat(conf.GetConfig().PrivateKeyPath); errors.Is(err, os.ErrNotExist) {
}() return errors.New("private key path does not exist")
if conf.GetConfig().SourceMode == "ldap" {
for {
ldap := auth_connectors.New()
roles, err := ldap.GetRoles(context.Background())
if err == nil {
logger.Info().Msgf("Syncing %d LDAP role groups to Keto", len(roles))
for _, role := range roles {
for r, m := range role.Members {
infrastructure.GetPermissionConnector("").CreateRole(r)
for _, p := range m {
infrastructure.GetPermissionConnector("").BindRole(r, p)
}
}
}
break
} else {
logger.Error().Msg("Failed to get LDAP roles, retrying in 10s: " + err.Error())
time.Sleep(10 * time.Second)
continue
}
}
} }
if _, err := os.Stat(conf.GetConfig().PublicKeyPath); errors.Is(err, os.ErrNotExist) {
return errors.New("public key path does not exist")
}
// check if peer already exists
p := requester.Search(nil,strconv.Itoa(peer.SELF.EnumIndex()))
// p := oclib.Search(nil, strconv.Itoa(peer.SELF.EnumIndex()), oclib.L ibDataEnum(oclib.PEER))
file := ""
f, err := os.ReadFile(conf.GetConfig().PublicKeyPath)
if err != nil {
return err
}
file = string(f)
if len(p.Data) > 0 {
// check public key with the one in the database
// compare the public key from file with the one in the database
if !strings.Contains(file, p.Data[0].(*peer.Peer).PublicKey) {
return errors.New("public key is different from the one in the database")
}
return nil
}
// create a new peer
o := oclib.GetConfLoader()
peer := &peer.Peer{
Url: o.GetStringDefault("HOSTNAME", "http://localhost"),
AbstractObject: utils.AbstractObject{
Name: o.GetStringDefault("NAME", "local"),
},
PublicKey: file,
State: peer.SELF,
}
data := requester.StoreOne(peer.Serialize(peer))
if data.Err != "" {
return errors.New(data.Err)
}
return nil
} }
func discovery() { func discovery() {
logger := oclib.GetLogger() api := tools.API{}
defer func() { conn := infrastructure.GetPermissionConnector()
if r := recover(); r != nil {
logger.Error().Msgf("discovery recovered from panic: %v", r) conn.CreateRole(conf.GetConfig().AdminRole)
} conn.BindRole(conf.GetConfig().AdminRole, "admin")
}() addPermissions := func(m map[string]interface{}) {
for { for k, v := range m {
api := tools.API{} for _, p := range v.([]interface{}) {
conn := infrastructure.GetPermissionConnector("") conn.CreatePermission(k, p.(string), true)
logger.Info().Msg("Starting permission discovery")
_, _, err := conn.CreateRole(conf.GetConfig().AdminRole)
if err != nil {
if !strings.Contains(err.Error(), "already exist") {
logger.Error().Msg("Failed to create admin role, retrying in 10s: " + err.Error())
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())
}
addPermissions := func(m tools.NATSResponse) {
var resp map[string][]interface{}
json.Unmarshal(m.Payload, &resp)
for k, v := range resp {
for _, p := range v {
if _, _, err := conn.CreatePermission(k, p.(string), true); err != nil {
logger.Error().Msg("Failed to admin create permission: " + err.Error())
}
}
}
}
api.ListenRouter(addPermissions)
b, _ := json.Marshal(map[string]interface{}{})
tools.NewNATSCaller().SetNATSPub(tools.DISCOVERY, tools.NATSResponse{
FromApp: "oc-auth",
Datatype: -1,
User: "root",
Method: tools.GET.EnumIndex(),
Payload: b,
})
break
} }
api.ListenRouter(addPermissions)
tools.NewNATSCaller().SetNATSPub("api", tools.DISCOVERY, map[string]interface{}{})
} }

BIN
oc-auth

Binary file not shown.

View File

@@ -79,15 +79,6 @@ func init() {
Filters: nil, Filters: nil,
Params: nil}) Params: nil})
beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"],
beego.ControllerComments{
Method: "Consent",
Router: `/consent`,
AllowHTTPMethods: []string{"get"},
MethodParams: param.Make(),
Filters: nil,
Params: nil})
beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"], beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"],
beego.ControllerComments{ beego.ControllerComments{
Method: "InternalAuthForward", Method: "InternalAuthForward",
@@ -108,17 +99,8 @@ func init() {
beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"], beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"],
beego.ControllerComments{ beego.ControllerComments{
Method: "GetLogin", Method: "LoginLDAP",
Router: `/login`, Router: `/ldap/login`,
AllowHTTPMethods: []string{"get"},
MethodParams: param.Make(),
Filters: nil,
Params: nil})
beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"],
beego.ControllerComments{
Method: "Login",
Router: `/login`,
AllowHTTPMethods: []string{"post"}, AllowHTTPMethods: []string{"post"},
MethodParams: param.Make(), MethodParams: param.Make(),
Filters: nil, Filters: nil,
@@ -126,17 +108,8 @@ func init() {
beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"], beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"],
beego.ControllerComments{ beego.ControllerComments{
Method: "GetLogout", Method: "LogOutLDAP",
Router: `/logout`, Router: `/ldap/logout`,
AllowHTTPMethods: []string{"get"},
MethodParams: param.Make(),
Filters: nil,
Params: nil})
beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"],
beego.ControllerComments{
Method: "LogOut",
Router: `/logout`,
AllowHTTPMethods: []string{"delete"}, AllowHTTPMethods: []string{"delete"},
MethodParams: param.Make(), MethodParams: param.Make(),
Filters: nil, Filters: nil,

View File

@@ -15,57 +15,18 @@
}, },
"basePath": "/oc/", "basePath": "/oc/",
"paths": { "paths": {
"/consent": {
"get": {
"tags": [
"oc-auth/controllersOAuthController"
],
"description": "Hydra redirects here with a consent_challenge. Auto-accepts consent with user permissions.\n\u003cbr\u003e",
"operationId": "OAuthController.Consent",
"parameters": [
{
"in": "query",
"name": "consent_challenge",
"description": "The consent challenge from Hydra",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "redirect",
"description": "explicit redirect by passed",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/auth_connectors.Redirect"
}
},
"400": {
"description": "missing consent_challenge"
},
"500": {
"description": "internal error"
}
}
}
},
"/forward": { "/forward": {
"get": { "get": {
"tags": [ "tags": [
"oc-auth/controllersOAuthController" "oc-auth/controllersOAuthController"
], ],
"description": "Forward auth for Traefik — validates JWT via Hydra introspection.\n\u003cbr\u003e", "description": "auth forward\n\u003cbr\u003e",
"operationId": "OAuthController.AuthForward", "operationId": "OAuthController.AuthForward",
"parameters": [ "parameters": [
{ {
"in": "header", "in": "header",
"name": "Authorization", "name": "Authorization",
"description": "Bearer token", "description": "auth token",
"type": "string" "type": "string"
} }
], ],
@@ -230,7 +191,7 @@
"parameters": [ "parameters": [
{ {
"in": "path", "in": "path",
"name": "user_id", "name": "group_id",
"description": "The group_id you want to unbind", "description": "The group_id you want to unbind",
"required": true, "required": true,
"type": "string" "type": "string"
@@ -255,169 +216,66 @@
"tags": [ "tags": [
"oc-auth/controllersOAuthController" "oc-auth/controllersOAuthController"
], ],
"description": "Introspect a token — respects Hydra's response\n\u003cbr\u003e", "description": "introspect token\n\u003cbr\u003e",
"operationId": "OAuthController.Introspect", "operationId": "OAuthController.Introspection",
"parameters": [ "parameters": [
{ {
"in": "header", "in": "header",
"name": "Authorization", "name": "Authorization",
"description": "Bearer token", "description": "auth token",
"type": "string" "type": "string"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "{string}"
"schema": {
"$ref": "#/definitions/auth_connectors.IntrospectResult"
}
} }
} }
} }
}, },
"/login": { "/ldap/login": {
"get": {
"tags": [
"oc-auth/controllersOAuthController"
],
"description": "Hydra redirects here with a login_challenge. Returns challenge info or auto-accepts if session exists.\n\u003cbr\u003e",
"operationId": "OAuthController.GetLogin",
"parameters": [
{
"in": "query",
"name": "login_challenge",
"description": "The login challenge from Hydra",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "redirect",
"description": "explicit redirect by passed",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/auth_connectors.LoginChallenge"
}
},
"400": {
"description": "missing login_challenge"
},
"500": {
"description": "internal error"
}
}
},
"post": { "post": {
"tags": [ "tags": [
"oc-auth/controllersOAuthController" "oc-auth/controllersOAuthController"
], ],
"description": "Authenticate user via LDAP and accept Hydra login challenge\n\u003cbr\u003e", "description": "authenticate user\n\u003cbr\u003e",
"operationId": "OAuthController.Login", "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": "The workflow content",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/auth_connectors.LoginRequest" "$ref": "#/definitions/models.workflow"
} }
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "{string}"
"schema": {
"$ref": "#/definitions/auth_connectors.Redirect"
}
},
"401": {
"description": "invalid credentials"
},
"500": {
"description": "internal error"
} }
} }
} }
}, },
"/logout": { "/ldap/logout": {
"get": {
"tags": [
"oc-auth/controllersOAuthController"
],
"description": "Hydra redirects here with a logout_challenge. Accepts the challenge and returns a redirect URL.\n\u003cbr\u003e",
"operationId": "OAuthController.GetLogout",
"parameters": [
{
"in": "query",
"name": "logout_challenge",
"description": "The logout challenge from Hydra",
"required": true,
"type": "string"
},
{
"in": "query",
"name": "redirect",
"description": "explicit redirect by passed",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/auth_connectors.Redirect"
}
},
"400": {
"description": "missing logout_challenge"
},
"500": {
"description": "internal error"
}
}
},
"delete": { "delete": {
"tags": [ "tags": [
"oc-auth/controllersOAuthController" "oc-auth/controllersOAuthController"
], ],
"description": "Revoke an OAuth2 token\n\u003cbr\u003e", "description": "unauthenticate user\n\u003cbr\u003e",
"operationId": "OAuthController.Logout", "operationId": "OAuthController.Logout",
"parameters": [ "parameters": [
{ {
"in": "header", "in": "header",
"name": "Authorization", "name": "Authorization",
"description": "Bearer token", "description": "auth token",
"type": "string"
},
{
"in": "query",
"name": "client_id",
"description": "The client_id",
"required": true,
"type": "string" "type": "string"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "{string}"
"schema": {
"$ref": "#/definitions/auth_connectors.Token"
}
} }
} }
} }
@@ -596,28 +454,22 @@
"tags": [ "tags": [
"oc-auth/controllersOAuthController" "oc-auth/controllersOAuthController"
], ],
"description": "Exchange a refresh_token for a new token set\n\u003cbr\u003e", "description": "introspect token\n\u003cbr\u003e",
"operationId": "OAuthController.Refresh", "operationId": "OAuthController.Introspection",
"parameters": [ "parameters": [
{ {
"in": "body", "in": "body",
"name": "body", "name": "body",
"description": "refresh_token and client_id", "description": "The token info",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/object" "$ref": "#/definitions/models.Token"
} }
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "", "description": "{string}"
"schema": {
"$ref": "#/definitions/auth_connectors.TokenResponse"
}
},
"401": {
"description": "invalid refresh token"
} }
} }
} }
@@ -826,152 +678,19 @@
} }
}, },
"definitions": { "definitions": {
"2432.0xc0004a0630.false": { "models.Token": {
"title": "false",
"type": "object"
},
"4171.0xc0004a0810.false": {
"title": "false",
"type": "object"
},
"auth_connectors.LoginRequest": {
"title": "LoginRequest",
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"login_challenge": {
"type": "string"
}
}
},
"auth_connectors.IntrospectResult": {
"title": "IntrospectResult",
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"client_id": {
"type": "string"
},
"exp": {
"type": "integer",
"format": "int64"
},
"ext": {
"$ref": "#/definitions/4171.0xc0004a0810.false"
},
"scope": {
"type": "string"
},
"sub": {
"type": "string"
},
"token_type": {
"type": "string"
}
}
},
"auth_connectors.LoginChallenge": {
"title": "LoginChallenge",
"type": "object",
"properties": {
"challenge": {
"type": "string"
},
"client": {
"$ref": "#/definitions/2432.0xc0004a0630.false"
},
"request_url": {
"type": "string"
},
"session_id": {
"type": "string"
},
"skip": {
"type": "boolean"
},
"subject": {
"type": "string"
}
}
},
"auth_connectors.Redirect": {
"title": "Redirect",
"type": "object",
"properties": {
"redirect_to": {
"type": "string"
}
}
},
"auth_connectors.Token": {
"title": "Token", "title": "Token",
"type": "object", "type": "object"
"properties": {
"access_token": {
"type": "string"
},
"active": {
"type": "boolean"
},
"expires_in": {
"type": "integer",
"format": "int64"
},
"id_token": {
"type": "string"
},
"refresh_token": {
"type": "string"
},
"scope": {
"type": "string"
},
"token_type": {
"type": "string"
}
}
}, },
"auth_connectors.TokenResponse": { "models.workflow": {
"title": "TokenResponse", "title": "workflow",
"type": "object",
"properties": {
"access_token": {
"type": "string"
},
"expires_in": {
"type": "integer",
"format": "int64"
},
"id_token": {
"type": "string"
},
"refresh_token": {
"type": "string"
},
"scope": {
"type": "string"
},
"token_type": {
"type": "string"
}
}
},
"object": {
"title": "object",
"type": "object" "type": "object"
} }
}, },
"tags": [ "tags": [
{ {
"name": "oc-auth/controllersOAuthController", "name": "oc-auth/controllersOAuthController",
"description": "OAuthController handles OAuth2 login/consent provider endpoints\n" "description": "Operations about auth\n"
}, },
{ {
"name": "group", "name": "group",

View File

@@ -12,46 +12,18 @@ info:
url: https://www.gnu.org/licenses/agpl-3.0.html url: https://www.gnu.org/licenses/agpl-3.0.html
basePath: /oc/ basePath: /oc/
paths: paths:
/consent:
get:
tags:
- oc-auth/controllersOAuthController
description: |-
Hydra redirects here with a consent_challenge. Auto-accepts consent with user permissions.
<br>
operationId: OAuthController.Consent
parameters:
- in: query
name: consent_challenge
description: The consent challenge from Hydra
required: true
type: string
- in: query
name: redirect
description: explicit redirect by passed
required: true
type: string
responses:
"200":
description: ""
schema:
$ref: '#/definitions/auth_connectors.Redirect'
"400":
description: missing consent_challenge
"500":
description: internal error
/forward: /forward:
get: get:
tags: tags:
- oc-auth/controllersOAuthController - oc-auth/controllersOAuthController
description: |- description: |-
Forward auth for Traefik — validates JWT via Hydra introspection. auth forward
<br> <br>
operationId: OAuthController.AuthForward operationId: OAuthController.AuthForward
parameters: parameters:
- in: header - in: header
name: Authorization name: Authorization
description: Bearer token description: auth token
type: string type: string
responses: responses:
"200": "200":
@@ -147,7 +119,7 @@ paths:
operationId: GroupController.UnBind operationId: GroupController.UnBind
parameters: parameters:
- in: path - in: path
name: user_id name: group_id
description: The group_id you want to unbind description: The group_id you want to unbind
required: true required: true
type: string type: string
@@ -192,125 +164,51 @@ paths:
tags: tags:
- oc-auth/controllersOAuthController - oc-auth/controllersOAuthController
description: |- description: |-
Introspect a token — respects Hydra's response introspect token
<br> <br>
operationId: OAuthController.Introspect operationId: OAuthController.Introspection
parameters: parameters:
- in: header - in: header
name: Authorization name: Authorization
description: Bearer token description: auth token
type: string type: string
responses: responses:
"200": "200":
description: "" description: '{string}'
schema: /ldap/login:
$ref: '#/definitions/auth_connectors.IntrospectResult'
/login:
get:
tags:
- oc-auth/controllersOAuthController
description: |-
Hydra redirects here with a login_challenge. Returns challenge info or auto-accepts if session exists.
<br>
operationId: OAuthController.GetLogin
parameters:
- in: query
name: login_challenge
description: The login challenge from Hydra
required: true
type: string
- in: query
name: redirect
description: explicit redirect by passed
required: true
type: string
responses:
"200":
description: ""
schema:
$ref: '#/definitions/auth_connectors.LoginChallenge'
"400":
description: missing login_challenge
"500":
description: internal error
post: post:
tags: tags:
- oc-auth/controllersOAuthController - oc-auth/controllersOAuthController
description: |- description: |-
Authenticate user via LDAP and accept Hydra login challenge authenticate user
<br> <br>
operationId: OAuthController.Login 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: The workflow content
required: true required: true
schema: schema:
$ref: '#/definitions/auth_connectors.LoginRequest' $ref: '#/definitions/models.workflow'
responses: responses:
"200": "200":
description: "" description: '{string}'
schema: /ldap/logout:
$ref: '#/definitions/auth_connectors.Redirect'
"401":
description: invalid credentials
"500":
description: internal error
/logout:
get:
tags:
- oc-auth/controllersOAuthController
description: |-
Hydra redirects here with a logout_challenge. Accepts the challenge and returns a redirect URL.
<br>
operationId: OAuthController.GetLogout
parameters:
- in: query
name: logout_challenge
description: The logout challenge from Hydra
required: true
type: string
- in: query
name: redirect
description: explicit redirect by passed
required: true
type: string
responses:
"200":
description: ""
schema:
$ref: '#/definitions/auth_connectors.Redirect'
"400":
description: missing logout_challenge
"500":
description: internal error
delete: delete:
tags: tags:
- oc-auth/controllersOAuthController - oc-auth/controllersOAuthController
description: |- description: |-
Revoke an OAuth2 token unauthenticate user
<br> <br>
operationId: OAuthController.Logout operationId: OAuthController.Logout
parameters: parameters:
- in: header - in: header
name: Authorization name: Authorization
description: Bearer token description: auth token
type: string
- in: query
name: client_id
description: The client_id
required: true
type: string type: string
responses: responses:
"200": "200":
description: "" description: '{string}'
schema:
$ref: '#/definitions/auth_connectors.Token'
/permission/: /permission/:
get: get:
tags: tags:
@@ -442,23 +340,19 @@ paths:
tags: tags:
- oc-auth/controllersOAuthController - oc-auth/controllersOAuthController
description: |- description: |-
Exchange a refresh_token for a new token set introspect token
<br> <br>
operationId: OAuthController.Refresh operationId: OAuthController.Introspection
parameters: parameters:
- in: body - in: body
name: body name: body
description: refresh_token and client_id description: The token info
required: true required: true
schema: schema:
$ref: '#/definitions/object' $ref: '#/definitions/models.Token'
responses: responses:
"200": "200":
description: "" description: '{string}'
schema:
$ref: '#/definitions/auth_connectors.TokenResponse'
"401":
description: invalid refresh token
/role/: /role/:
get: get:
tags: tags:
@@ -613,106 +507,16 @@ paths:
"200": "200":
description: "" description: ""
definitions: definitions:
2432.0xc0004a0630.false: models.Token:
title: "false"
type: object
4171.0xc0004a0810.false:
title: "false"
type: object
auth_connectors.LoginRequest:
title: LoginRequest
type: object
properties:
username:
type: string
password:
type: string
login_challenge:
type: string
auth_connectors.IntrospectResult:
title: IntrospectResult
type: object
properties:
active:
type: boolean
client_id:
type: string
exp:
type: integer
format: int64
ext:
$ref: '#/definitions/4171.0xc0004a0810.false'
scope:
type: string
sub:
type: string
token_type:
type: string
auth_connectors.LoginChallenge:
title: LoginChallenge
type: object
properties:
challenge:
type: string
client:
$ref: '#/definitions/2432.0xc0004a0630.false'
request_url:
type: string
session_id:
type: string
skip:
type: boolean
subject:
type: string
auth_connectors.Redirect:
title: Redirect
type: object
properties:
redirect_to:
type: string
auth_connectors.Token:
title: Token title: Token
type: object type: object
properties: models.workflow:
access_token: title: workflow
type: string
active:
type: boolean
expires_in:
type: integer
format: int64
id_token:
type: string
refresh_token:
type: string
scope:
type: string
token_type:
type: string
auth_connectors.TokenResponse:
title: TokenResponse
type: object
properties:
access_token:
type: string
expires_in:
type: integer
format: int64
id_token:
type: string
refresh_token:
type: string
scope:
type: string
token_type:
type: string
object:
title: object
type: object type: object
tags: tags:
- name: oc-auth/controllersOAuthController - name: oc-auth/controllersOAuthController
description: | description: |
OAuthController handles OAuth2 login/consent provider endpoints Operations about auth
- name: group - name: group
description: | description: |
Operations about auth Operations about auth