65 Commits

Author SHA1 Message Date
mr
bc7f0be53b Auth rebuild 2026-04-02 09:59:53 +02:00
mr
284667e95c Forward For WS 2026-04-01 17:16:18 +02:00
mr
744caf9a5e Fully Working OAuth2Flow 2026-03-06 10:20:35 +01:00
mr
979747e288 Complete and refine OAuth + Traeffik Restriction 2026-02-20 10:30:34 +01:00
mr
078aae8172 oc-auth OAUTH2 2026-02-19 14:56:15 +01:00
mr
048707bfe5 Bypass mode 2026-02-17 10:16:18 +01:00
mr
5f7289bb05 a.getPath(true, true) 2026-02-10 10:53:13 +01:00
mr
484154a48d auth 2026-02-10 10:13:33 +01:00
mr
af0a8cb117 oc-auth 2026-02-10 09:39:13 +01:00
mr
eb6dee0c4d lightest tag 2026-02-09 09:42:19 +01:00
mr
b8b0743af5 dockerfile scratch 2026-02-09 09:01:09 +01:00
mr
28c08d0873 remove git add 2026-02-09 08:53:05 +01:00
mr
eef21ad537 publish ci 2026-02-05 12:01:17 +01:00
mr
492aff13a3 publish-registry ci 2026-02-05 11:59:43 +01:00
mr
627058fcab publish-registry 2026-02-05 11:55:09 +01:00
mr
1acd2ea634 LDAPRoleBaseDN 2026-02-04 11:28:03 +01:00
mr
a897f5aa75 user not found 2026-02-04 11:06:38 +01:00
mr
64b8da67f2 inspect conn 2026-02-04 10:59:01 +01:00
mr
5512cc76c3 users 2026-02-04 10:47:40 +01:00
mr
7d3cb1af61 security injection appname 2026-02-04 09:42:20 +01:00
mr
b18718cb47 compact conf 2026-02-03 16:20:25 +01:00
mr
3f245b3f02 reversuserldap 2026-02-03 10:04:10 +01:00
mr
980fd50cf5 oclib-debug 2026-02-03 08:50:06 +01:00
mr
02476ca07d uid=admin,ou=users,dc=opencloud,dc=com 2026-02-02 11:02:27 +01:00
mr
f56b947e1f LDAP_USER_BASEDN 2026-02-02 10:36:31 +01:00
mr
0e86777fd3 ok 2026-02-02 10:09:45 +01:00
mr
6b9e21b929 test 2026-02-02 10:00:44 +01:00
mr
365d62a64e User Base DN 2026-01-26 11:05:38 +01:00
mr
076dca0a1d new oclib 2026-01-26 10:38:39 +01:00
mr
e1cb9b3a08 debug recovery 2026-01-26 09:48:21 +01:00
mr
7127dc9010 prospect failing binding 2026-01-26 09:17:35 +01:00
mr
1f4b25c594 test 2026-01-23 11:09:31 +01:00
mr
f93371e449 ldap -> auth 2026-01-23 10:22:14 +01:00
mr
013c6969c5 6b12aa1713c79983dc99e489acb2d4e0da641b7d 2026-01-23 10:04:59 +01:00
mr
403deaf65b test 2026-01-23 09:49:46 +01:00
mr
9d0b720231 new oclib 2026-01-23 09:40:38 +01:00
mr
932e40190d CLUSTERNAME in makefile 2026-01-20 11:20:16 +01:00
mr
f226866fc7 dockerfile 2026-01-08 10:41:02 +01:00
mr
b154532a1a update 2025-11-20 16:31:10 +01:00
mr
a546c1220e gitignore 2025-11-13 09:57:40 +01:00
fb3366328b Ajouter .gitattributes 2025-11-01 16:38:21 +01:00
mr
75857dc125 oclib 2025-06-24 16:57:35 +02:00
mr
e7ff288972 nats push 2025-06-24 09:14:59 +02:00
mr
d83208be52 deploy adjust 2025-06-16 09:11:21 +02:00
mr
3d42ce6820 auth 2025-04-01 10:16:26 +02:00
mr
5ca9a10d14 launch mode 2025-03-06 09:46:13 +01:00
mr
a480c9b8a0 neo oclib 2025-02-21 11:24:03 +01:00
mr
6a6fe77c30 traefik 2025-02-19 12:02:44 +01:00
mr
2f8524af01 oclib update 2025-02-18 15:06:32 +01:00
mr
b684ba841f Correction 2025-02-18 09:20:13 +01:00
mr
37a0ceddf4 adjust in docker conf 2025-02-18 08:52:47 +01:00
mr
b18b82ea8c Merge branch 'feature/order' into main 2025-02-18 08:35:12 +01:00
mr
9bb08fc961 Merge branch 'feature/payment' into main 2025-02-13 10:32:46 +01:00
mr
cf08618f83 neo oclib 2025-02-13 10:28:36 +01:00
plm
8df956bdcd Handling clientID/password from k8s secret 2025-01-22 15:23:18 +01:00
plm
776aac5d43 Fix oc-auth for k8s integration 2025-01-21 15:23:45 +01:00
plm
27e2df2310 Support CORS 2025-01-15 11:38:12 +01:00
plm
939c8cdd67 Updating go.sum 2025-01-08 21:55:45 +01:00
plm
2a794518d5 upgrading oc-lib 2025-01-08 21:44:50 +01:00
plm
4498afabac Fix dependencies version 2024-12-16 14:26:47 +01:00
plm
f10615888c Pointing on last issue#4 commit to use oc-lib env var fix, removing useless log in Dockerfile building phase 2024-12-16 10:18:23 +01:00
plm
2ce3a380f0 Updating dependencies + fix dockerfile for quicker build + Introducing top Makefile script as unique entry point for building, deployment and high level tasks 2024-12-16 09:14:40 +01:00
plm
36e843d343 Fixing perm connector key (cant be based on perm service url) 2024-12-16 09:12:28 +01:00
plm
3a30e265cf Remove production binary from conf 2024-12-16 09:11:35 +01:00
plm
4add83b0d6 Removing debug binary from conf 2024-12-16 09:11:01 +01:00
35 changed files with 2457 additions and 2059 deletions

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
# 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

@@ -1,32 +1,43 @@
FROM golang:alpine as builder FROM golang:alpine AS deps
ARG HOSTNAME=http://localhost
ARG NAME=local
WORKDIR /app WORKDIR /app
COPY go.mod go.sum ./
RUN sed -i '/replace/d' go.mod
RUN cat go.mod
RUN go mod download
#----------------------------------------------------------------------------------------------
FROM golang:alpine AS builder
RUN go install github.com/beego/bee/v2@latest
WORKDIR /oc-auth
COPY --from=deps /go/pkg /go/pkg
COPY --from=deps /app/go.mod /app/go.sum ./
RUN export CGO_ENABLED=0 && \
export GOOS=linux && \
export GOARCH=amd64 && \
export BUILD_FLAGS="-ldflags='-w -s'"
COPY . . COPY . .
RUN apk add git RUN sed -i '/replace/d' go.mod
RUN bee pack
RUN mkdir -p /app/extracted && tar -zxvf oc-auth.tar.gz -C /app/extracted
RUN sed -i 's/http:\/\/127.0.0.1:8080\/swagger\/swagger.json/swagger.json/g' /app/extracted/swagger/index.html
RUN go get github.com/beego/bee/v2 && go install github.com/beego/bee/v2@master #----------------------------------------------------------------------------------------------
RUN timeout 15 bee run -gendoc=true -downdoc=true -runmode=dev || : FROM golang:alpine
RUN sed -i 's/http:\/\/127.0.0.1:8080\/swagger\/swagger.json/swagger.json/g' swagger/index.html
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" .
RUN ls /app
FROM scratch
WORKDIR /app WORKDIR /app
COPY --from=builder /app/extracted/oc-auth /usr/bin
COPY --from=builder /app/oc-auth /usr/bin/ COPY --from=builder /app/extracted/swagger /app/swagger
COPY --from=builder /app/swagger /app/swagger COPY --from=builder /app/extracted/pem /app/pem
COPY --from=builder /app/extracted/docker_auth.json /etc/oc/auth.json
COPY docker_auth.json /etc/oc/auth.json
EXPOSE 8080 EXPOSE 8080

42
Makefile Normal file
View File

@@ -0,0 +1,42 @@
.DEFAULT_GOAL := all
build: clean
bee pack
run:
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:
bee run -downdebug -gendebug
clean:
rm -rf oc-auth oc-auth.tar.gz
docker:
DOCKER_BUILDKIT=1 docker build -t oc-auth -f Dockerfile . --build-arg=HOST=$(HOST)
docker tag oc-auth opencloudregistry/oc-auth:latest
publish-kind:
kind load docker-image opencloudregistry/oc-auth:latest --name $(CLUSTER_NAME) | true
publish-registry:
docker push opencloudregistry/oc-auth:latest
docker-deploy:
docker compose up -d
run-docker: docker publish-kind publish-registry docker-deploy
all: docker publish-kind
ci: docker publish-registry
.PHONY: build run clean docker publish-kind publish-registry

View File

@@ -7,7 +7,76 @@ 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

Binary file not shown.

View File

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

View File

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

View File

@@ -12,18 +12,38 @@ 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 int AuthConnectorAdminPort string
PermissionConnectorHost string PermissionConnectorWriteHost string
PermissionConnectorPort int PermissionConnectorReadHost string
PermissionConnectorAdminPort int PermissionConnectorPort string
PermissionConnectorAdminPort string
AdminOrigin string
Origin string
// OAuth2ClientID is the client_id registered in Hydra, used to initiate the authorization flow.
OAuth2ClientID string
// OAuth2AdminClientID is the client_id for the admin frontend.
OAuth2AdminClientID string
// OAuthRedirectURI is the registered OAuth2 redirect_uri (frontend login/callback URL).
// Hydra redirects here with login_challenge (login phase) or authorization code (callback phase).
OAuthRedirectURI string
OAdminAuthRedirectURI string
Local bool
} }
var instance *Config var instance *Config

View File

@@ -5,174 +5,613 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"oc-auth/conf" "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"
"sync"
"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"
) )
// Operations about auth var selfPeerCache struct {
sync.RWMutex
peer *model.Peer
fetchedAt time.Time
}
const selfPeerCacheTTL = 60 * time.Second
func getCachedSelfPeer() *model.Peer {
selfPeerCache.RLock()
if selfPeerCache.peer != nil && time.Since(selfPeerCache.fetchedAt) < selfPeerCacheTTL {
p := selfPeerCache.peer
selfPeerCache.RUnlock()
return p
}
selfPeerCache.RUnlock()
pp := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil).Search(
&dbs.Filters{
Or: map[string][]dbs.Filter{
"relation": {{Operator: dbs.EQUAL.String(), Value: peer.SELF}},
},
}, strconv.Itoa(peer.SELF.EnumIndex()), false)
if len(pp.Data) == 0 || pp.Code >= 300 || pp.Err != "" {
return nil
}
p := pp.Data[0].(*model.Peer)
selfPeerCache.Lock()
selfPeerCache.peer = p
selfPeerCache.fetchedAt = time.Now()
selfPeerCache.Unlock()
return p
}
// InvalidateSelfPeerCache forces the next call to getCachedSelfPeer to re-fetch from DB.
func InvalidateSelfPeerCache() {
selfPeerCache.Lock()
selfPeerCache.peer = nil
selfPeerCache.Unlock()
}
// OAuthController handles OAuth2 login/consent provider endpoints
type OAuthController struct { type OAuthController struct {
beego.Controller beego.Controller
} }
// @Title Logout // @Title GetLogin
// @Description unauthenticate user // @Description Hydra redirects here with a login_challenge. Returns challenge info or auto-accepts if session exists.
// @Param Authorization header string false "auth token" // @Param login_challenge query string true "The login challenge from Hydra"
// @Param client_id query string true "the client_id you want to get" // @Param client_id query string true "The targetted client_id from Hydra"
// @Success 200 {string} // @Param redirect query string true "explicit redirect by passed"
// @router /logout [delete]
func (o *OAuthController) LogOut() {
// authorize user
clientID := o.Ctx.Input.Query("client_id")
reqToken := o.Ctx.Request.Header.Get("Authorization")
splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) < 2 {
reqToken = ""
} else {
reqToken = splitToken[1]
}
var res auth_connectors.Token
json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &res)
token, err := infrastructure.GetAuthConnector().Logout(clientID, reqToken) // @Success 200 {object} auth_connectors.LoginChallenge
if err != nil || token == nil { // @Failure 400 missing login_challenge
o.Data["json"] = err // @Failure 500 internal error
} else { // @router /login [get]
o.Data["json"] = token func (o *OAuthController) GetLogin() {
} logger := oclib.GetLogger()
o.ServeJSON() challenge := o.Ctx.Input.Query("login_challenge")
}
// @Title Login
// @Description authenticate user
// @Param body body models.workflow true "The workflow content"
// @Param client_id query string true "the client_id you want to get"
// @Success 200 {string}
// @router /login [post]
func (o *OAuthController) Login() {
// authorize user
fmt.Println("Login", o.Ctx.Input.Query("client_id"), o.Ctx.Input.Param(":client_id"))
clientID := o.Ctx.Input.Query("client_id") clientID := o.Ctx.Input.Query("client_id")
var res auth_connectors.Token if challenge == "" {
json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &res) // No challenge yet — initiate the OAuth2 flow server-side to get one from Hydra.
if conf.GetConfig().SourceMode == "ldap" { // This supports thick clients that cannot follow browser redirects.
ldap := auth_connectors.New() freshChallenge, err := infrastructure.GetAuthConnector().InitiateLogin(clientID, "")
found, err := ldap.Authenticate(o.Ctx.Request.Context(), res.Username, res.Password) if err != nil {
fmt.Println("found", found, "err", err) logger.Error().Msg("Failed to initiate login: " + err.Error())
if err != nil || !found { o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = err o.Data["json"] = map[string]string{"error": err.Error()}
o.Ctx.ResponseWriter.WriteHeader(401)
o.ServeJSON() o.ServeJSON()
return return
} }
loginChallenge, err := infrastructure.GetAuthConnector().GetLoginChallenge(freshChallenge)
if err != nil {
logger.Error().Msg("Failed to get fresh login challenge: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
o.ServeJSON()
return
}
o.Data["json"] = loginChallenge
o.ServeJSON()
return
} }
token, err := infrastructure.GetAuthConnector().Login(
clientID, res.Username, if conf.GetConfig().Local {
&http.Cookie{ // open a session // In local mode, return a mock challenge for dev
Name: "csrf_token", o.Data["json"] = &auth_connectors.LoginChallenge{
Value: o.XSRFToken(), Skip: false,
}) Challenge: challenge,
fmt.Println("token", token, "err", err) }
if err != nil || token == nil { o.ServeJSON()
o.Data["json"] = err 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 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 return_mode query string false "Return mode: 'redirect' (default, 303), 'json' (full object), 'token' (access token string)"
// @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()
returnMode := o.Ctx.Input.Query("return_mode")
if returnMode == "" {
returnMode = "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
}
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
}
// 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.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),
}
if returnMode == "token" {
o.Ctx.ResponseWriter.Header().Set("Content-Type", "text/plain")
o.Ctx.ResponseWriter.WriteHeader(200)
o.Ctx.ResponseWriter.Write([]byte(token.AccessToken))
return
}
o.Data["json"] = token
} else {
o.Ctx.ResponseWriter.WriteHeader(401)
o.Data["json"] = map[string]string{"error": "peer not found"}
}
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 according to requested mode
switch returnMode {
case "token", "json":
tokenResp, err := completeFlowToToken(redirect.RedirectTo, req.Username, req.LoginChallenge)
if err != nil {
logger.Error().Msg("Failed to complete OAuth2 flow: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
o.ServeJSON()
return
}
if returnMode == "token" {
o.Ctx.ResponseWriter.Header().Set("Content-Type", "text/plain")
o.Ctx.ResponseWriter.WriteHeader(200)
o.Ctx.ResponseWriter.Write([]byte(tokenResp.AccessToken))
return
}
o.Data["json"] = tokenResp
o.ServeJSON()
default: // "redirect"
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)
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
// @Description Revoke an OAuth2 token
// @Param Authorization header string false "Bearer token"
// @Param client_id query string true "The client_id"
// @Success 200 {object} auth_connectors.Token
// @router /logout [delete]
func (o *OAuthController) LogOut() {
clientID := o.Ctx.Input.Query("client_id")
reqToken := extractBearerToken(o.Ctx.Request)
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 {
o.Data["json"] = &auth_connectors.Token{
AccessToken: reqToken,
Active: false,
}
}
o.ServeJSON()
}
// @Title Refresh
// @Description Exchange a refresh_token for a new token set
// @Param body body object true "refresh_token and client_id"
// @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 Introspection // @Title Introspect
// @Description introspect token // @Description Introspect a token — respects Hydra's response
// @Param body body models.Token true "The token info" // @Param Authorization header string false "Bearer token"
// @Param client_id query string true "the client_id you want to get" // @Success 200 {object} auth_connectors.IntrospectResult
// @Success 200 {string}
// @router /refresh [post]
func (o *OAuthController) Refresh() {
clientID := o.Ctx.Input.Query("client_id")
var token auth_connectors.Token
json.Unmarshal(o.Ctx.Input.CopyBody(100000), &token)
// refresh token
newToken, err := infrastructure.GetAuthConnector().Refresh(clientID, &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] // @router /introspect [get]
func (o *OAuthController) Introspect() { func (o *OAuthController) Introspect() {
reqToken := o.Ctx.Request.Header.Get("Authorization") reqToken := extractBearerToken(o.Ctx.Request)
splitToken := strings.Split(reqToken, "Bearer ") if reqToken == "" {
if len(splitToken) < 2 { o.Ctx.ResponseWriter.WriteHeader(401)
reqToken = "" o.Data["json"] = map[string]string{"error": "missing bearer token"}
} else { o.ServeJSON()
reqToken = splitToken[1] return
} }
token, err := infrastructure.GetAuthConnector().Introspect(reqToken) if conf.GetConfig().Local {
if err != nil || !token { o.Data["json"] = &auth_connectors.IntrospectResult{Active: true}
o.Data["json"] = err o.ServeJSON()
return
}
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 {
o.Data["json"] = result
} }
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 auth forward // @Description Forward auth for Traefik — validates JWT via Hydra introspection.
// @Param Authorization header string false "auth token" // Only requests from our own peer (SELF) are authorized.
// Routes in pathWhitelist bypass all checks (with or without token).
// Routes in whitelist bypass the token check (oc-auth own challenge endpoints).
// On missing/invalid token: 302 to Hydra authorization URL (restart OAuth2 flow).
// On wrong peer: 401 (network/config issue, no redirect).
// On valid token but insufficient permissions: 403.
// On success: 200 so Traefik forwards the request to the target route.
// @Param Authorization header string false "Bearer token"
// @Success 200 {string} // @Success 200 {string}
// @router /forward [get] // @router /forward [get]
func (o *OAuthController) InternaisDraftlAuthForward() { func (o *OAuthController) InternalAuthForward() {
fmt.Println("InternalAuthForward") fmt.Println("InternalAuthForward")
uri := o.Ctx.Request.Header.Get("X-Replaced-Path")
if uri == "" {
uri = o.Ctx.Request.Header.Get("X-Forwarded-Uri")
}
for _, w := range whitelist {
if strings.Contains(uri, w) {
fmt.Println("WHITELIST", w)
o.Ctx.ResponseWriter.WriteHeader(http.StatusOK)
return
}
}
origin, publicKey, external := o.extractOrigin(o.Ctx.Request)
reqToken := o.Ctx.Request.Header.Get("Authorization") reqToken := o.Ctx.Request.Header.Get("Authorization")
if reqToken == "" { if reqToken == "" {
for _, w := range whitelist { // WebSocket upgrade: the browser cannot send custom headers, so the token
if strings.Contains(o.Ctx.Request.Header.Get("X-Forwarded-Uri"), w) { // is passed as the Sec-WebSocket-Protocol subprotocol value instead.
o.Ctx.ResponseWriter.WriteHeader(200) if proto := o.Ctx.Request.Header.Get("Sec-WebSocket-Protocol"); proto != "" {
o.ServeJSON() reqToken = "Bearer " + proto
return
}
} }
o.Ctx.ResponseWriter.WriteHeader(401) }
o.ServeJSON() if reqToken == "" {
// Step 1: no token — allow oc-auth's own challenge endpoints (no token needed).
// No token and not a whitelisted path → restart OAuth2 flow.
fmt.Println("NO TOKEN")
o.redirectToLogin(origin)
return return
} }
// Step 2: extract Bearer token — malformed header treated as missing token.
splitToken := strings.Split(reqToken, "Bearer ") splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) < 2 { if len(splitToken) < 2 || splitToken[1] == "" {
reqToken = "" fmt.Println("MALFORMED BEARER")
} else { o.redirectToLogin(origin)
reqToken = splitToken[1] return
} }
origin, publicKey, external := o.extractOrigin(o.Ctx.Request) reqToken = splitToken[1]
if !infrastructure.GetAuthConnector().CheckAuthForward( //reqToken != "" &&
// Step 3: verify the token belongs to our self peer.
// Decode the JWT payload and extract ext.peer_id, then compare against the cached self peer UUID.
// A mismatch means the request comes from a foreign peer → 401 (not a login problem).
tokenPeerID := extractPeerIDFromToken(reqToken)
selfPeer := getCachedSelfPeer()
fmt.Println("TOKEN", tokenPeerID, selfPeer.UUID)
if selfPeer == nil || tokenPeerID != selfPeer.UUID {
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.
introspection, permissionKey, code := infrastructure.GetAuthConnector().CheckAuthForward(
reqToken, publicKey, origin, reqToken, publicKey, origin,
o.Ctx.Request.Header.Get("X-Forwarded-Method"), o.Ctx.Request.Header.Get("X-Forwarded-Method"),
o.Ctx.Request.Header.Get("X-Forwarded-Uri"), external) && origin != "" && publicKey != "" { uri, external)
o.Ctx.ResponseWriter.WriteHeader(401) switch code {
o.ServeJSON() case http.StatusOK:
return user, _, _ := oclib.ExtractTokenInfo(*o.Ctx.Request)
claims := infrastructure.GetClaims().BuildConsentSession(conf.GetConfig().OAuth2ClientID, user, selfPeer)
if !claims.EqualClaims(introspection, permissionKey) {
fmt.Println("Token is not fresh or compromised")
o.Ctx.ResponseWriter.WriteHeader(http.StatusConflict)
return
}
fmt.Println("OK")
o.Ctx.ResponseWriter.WriteHeader(http.StatusOK)
case http.StatusForbidden:
fmt.Println("StatusForbidden")
o.Ctx.ResponseWriter.WriteHeader(http.StatusForbidden)
default:
fmt.Println("redirectToLogin UNAUTHORIZED")
// 401 or unexpected status → token likely expired, restart the OAuth2 flow.
o.redirectToLogin(origin)
} }
o.ServeJSON() }
// redirectToLogin redirects the client to Hydra's authorization endpoint to start a fresh
// OAuth2 flow. Hydra will generate a login_challenge and redirect to the configured login URL.
func (o *OAuthController) redirectToLogin(origin string) {
cfg := conf.GetConfig()
var clientID, redirectURI string
if strings.Contains(origin, cfg.AdminOrigin) {
clientID = cfg.OAuth2AdminClientID
redirectURI = cfg.OAdminAuthRedirectURI
} else {
clientID = cfg.OAuth2ClientID
redirectURI = cfg.OAuthRedirectURI
}
hydraAuthURL := fmt.Sprintf("http://%s:%d/oauth2/auth?client_id=%s&response_type=code&redirect_uri=%s&scope=openid",
cfg.AuthConnectPublicHost,
cfg.AuthConnectorPort,
url.QueryEscape(clientID),
url.QueryEscape(redirectURI),
)
o.Ctx.ResponseWriter.Header().Set("Location", hydraAuthURL)
o.Ctx.ResponseWriter.WriteHeader(http.StatusFound)
} }
func (o *OAuthController) extractOrigin(request *http.Request) (string, string, bool) { func (o *OAuthController) extractOrigin(request *http.Request) (string, string, bool) {
@@ -189,15 +628,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)
} }
peer := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), user, peerID, groups, nil).Search(nil, searchStr, false) pp := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), user, peerID, groups, nil).Search(nil, searchStr, false)
if peer.Code != 200 || len(peer.Data) == 0 { // TODO: add state of partnership if pp.Code != 200 || len(pp.Data) == 0 {
return "", "", external return "", "", external
} }
p := peer.Data[0].(*model.Peer) p := pp.Data[0].(*model.Peer)
publicKey = p.PublicKey publicKey = p.PublicKey
origin = p.Url origin = p.APIUrl
if origin != "" { // is external if origin != "" {
if strings.Contains(origin, "localhost") || strings.Contains(origin, "127.0.0.1") || p.State == model.SELF { if p.Relation == peer.SELF {
external = false external = false
} }
} else { } else {
@@ -206,28 +645,151 @@ 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 { func ExtractClient(request http.Request) string {
reqToken := request.Header.Get("Authorization") reqToken := request.Header.Get("Authorization")
splitToken := strings.Split(reqToken, "Bearer ") splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) < 2 { if len(splitToken) < 2 {
reqToken = "" return ""
} else {
reqToken = splitToken[1]
} }
if reqToken != "" { reqToken = splitToken[1]
token := strings.Split(reqToken, ".") if reqToken == "" {
if len(token) > 2 { return ""
bytes, err := base64.StdEncoding.DecodeString(token[2]) }
// 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 { if err != nil {
return "" return ""
} }
m := map[string]interface{}{} }
err = json.Unmarshal(bytes, &m) m := map[string]interface{}{}
if err != nil { if err := json.Unmarshal(bytes, &m); err != nil {
return "" 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 m["session"].(map[string]interface{})["id_token"].(map[string]interface{})["client_id"].(string)
} }
} }
return "" return ""
} }
// completeFlowToToken drives the server-side OAuth2 flow after AcceptLogin.
// It follows Hydra's redirect to grab the consent_challenge, accepts it,
// then exchanges the resulting auth code for a token.
func completeFlowToToken(loginRedirectTo string, subject string, loginChallenge string) (*auth_connectors.TokenResponse, error) {
connector := infrastructure.GetAuthConnector()
// Step 1: follow the login redirect to get the consent_challenge (uses CSRF cookie from InitiateLogin)
consentChallenge, err := connector.FollowToConsentChallenge(loginRedirectTo, loginChallenge)
if err != nil {
return nil, fmt.Errorf("consent challenge: %w", err)
}
// Step 2: fetch consent challenge details (scopes + client_id)
consentDetails, err := connector.GetConsentChallenge(consentChallenge)
if err != nil {
return nil, fmt.Errorf("get consent challenge: %w", err)
}
clientID := ""
if consentDetails.Client != nil {
if cid, ok := consentDetails.Client["client_id"].(string); ok {
clientID = cid
}
}
// Step 3: get self peer for claims
pp := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil).Search(
&dbs.Filters{
Or: map[string][]dbs.Filter{
"relation": {{Operator: dbs.EQUAL.String(), Value: peer.SELF}},
},
}, strconv.Itoa(peer.SELF.EnumIndex()), false)
if len(pp.Data) == 0 || pp.Code >= 300 || pp.Err != "" {
return nil, fmt.Errorf("self peer not found")
}
p := pp.Data[0].(*model.Peer)
// Step 4: accept consent
session := infrastructure.GetClaims().BuildConsentSession(clientID, subject, p)
consentRedirect, err := connector.AcceptConsent(consentChallenge, consentDetails.RequestedScope, session)
if err != nil {
return nil, fmt.Errorf("accept consent: %w", err)
}
// Step 5: follow consent redirect to exchange auth code for token (uses CSRF cookie, cleans up jar)
token, err := connector.ExchangeCodeForToken(consentRedirect.RedirectTo, clientID, loginChallenge)
if err != nil {
return nil, fmt.Errorf("exchange code: %w", err)
}
return token, nil
}
// extractPeerIDFromToken decodes the JWT payload and returns ext.peer_id.
func extractPeerIDFromToken(token string) string {
parts := strings.Split(token, ".")
if len(parts) < 2 {
return ""
}
payload := parts[1]
switch len(payload) % 4 {
case 2:
payload += "=="
case 3:
payload += "="
}
b, err := base64.URLEncoding.DecodeString(payload)
if err != nil {
return ""
}
var claims map[string]interface{}
if err := json.Unmarshal(b, &claims); err != nil {
return ""
}
ext, ok := claims["ext"].(map[string]interface{})
if !ok {
return ""
}
peerID, _ := ext["peer_id"].(string)
return peerID
}
// 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

@@ -14,7 +14,10 @@ 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{"version": "1"} c.Data["json"] = map[string]string{
"service": "oc-auth",
"version": "1",
}
c.ServeJSON() c.ServeJSON()
} }
@@ -23,6 +26,9 @@ 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{"version": "1"} c.Data["json"] = map[string]string{
"service": "oc-auth",
"version": "1",
}
c.ServeJSON() c.ServeJSON()
} }

View File

@@ -1,21 +0,0 @@
version: '3.4'
services:
oc-auth-2:
image: 'oc-auth-2:latest'
ports:
- 8095:8080
container_name: oc-auth-2
environment:
LDAP_ENDPOINTS: ldap-2:389
LDAP_BINDDN: cn=admin,dc=example,dc=com
LDAP_BINDPW: password
LDAP_BASEDN: "dc=example,dc=com"
LDAP_ROLE_BASEDN: "ou=AppRoles,dc=example,dc=com"
networks:
- catalog
volumes:
- ./pem:/etc/oc/pem
networks:
catalog:
external: true

View File

@@ -1,22 +1,6 @@
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:
@@ -24,18 +8,27 @@ services:
container_name: oc-auth container_name: oc-auth
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.middlewares.auth.forwardauth.address=http://oc-auth:8080/oc/forward" - "traefik.http.routers.auth-sec.entrypoints=web"
- "traefik.http.routers.workflow.rule=PathPrefix(/auth)" - "traefik.http.routers.auth-sec.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:
- catalog - oc
volumes: volumes:
- ./pem:/etc/oc/pem - ./pem/private.pem:/keys/private/private.pem
- ./pem/public.pem:/keys/public/public.pem
networks: networks:
catalog: oc:
external: true external: true

View File

@@ -2,9 +2,10 @@
"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",
"PRIVATE_KEY_PATH": "/etc/oc/pem/private.pem", "AUTH_CONNECTOR_PUBLIC_HOST": "hydra",
"PUBLIC_KEY_PATH": "/etc/oc/pem/public.pem", "PRIVATE_KEY_PATH": "/keys/private/private.pem",
"LDAP_ENDPOINTS": "ldap:389" "PUBLIC_KEY_PATH": "/keys/public/public.pem",
"LDAP_ENDPOINTS": "ldap:389",
"LOCAL": false
} }

4
env.env Normal file
View File

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

96
go.mod
View File

@@ -1,85 +1,28 @@
module oc-auth module oc-auth
go 1.22.0 go 1.24.6
require ( require (
cloud.o-forge.io/core/oc-lib v0.0.0-20250205160221-88b7cfe2fd0f cloud.o-forge.io/core/oc-lib v0.0.0-20260219084344-9662ac6d678c
github.com/beego/beego/v2 v2.3.1 github.com/beego/beego/v2 v2.3.1
github.com/nats-io/nats.go v1.37.0
github.com/ory/hydra-client-go v1.11.8
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
golang.org/x/oauth2 v0.23.0
) )
//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/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/biter777/countries v1.7.5 // indirect github.com/biter777/countries v1.7.5 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.6.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/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobuffalo/pop/v6 v6.0.8 // indirect
github.com/gofrs/uuid v4.3.0+incompatible // indirect github.com/gofrs/uuid v4.3.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/libp2p/go-libp2p/core v0.43.0-rc2 // indirect
github.com/golang/glog v1.2.0 // indirect github.com/nats-io/nats.go v1.37.0 // indirect
github.com/golang/mock v1.6.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.2 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/marcinwyszynski/geopoint v0.0.0-20140302213024-cf2a6f750c5b // indirect
github.com/mattn/goveralls v0.0.12 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/openzipkin/zipkin-go v0.4.1 // indirect
github.com/ory/go-acc v0.2.9-0.20230103102148-6b1c9a70dbbe // indirect
github.com/ory/go-convenience v0.1.0 // indirect
github.com/ory/x v0.0.575 // indirect
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.42.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
go.opentelemetry.io/contrib/samplers/jaegerremote v0.11.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 // indirect
go.opentelemetry.io/otel/exporters/zipkin v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/grpc v1.63.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
) )
require ( require (
@@ -91,18 +34,15 @@ require (
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.22.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.7.0
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
github.com/goraz/onion v0.1.3 // indirect github.com/goraz/onion v0.1.3 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/i-core/rlog v1.0.0 github.com/i-core/rlog v1.0.0
github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/justinas/nosurf v1.1.1
github.com/kelseyhightower/envconfig v1.4.0
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.17.11 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
@@ -111,13 +51,10 @@ require (
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.7 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/ory/fosite v0.47.0
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.60.1 // indirect
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/purnaresa/bulwark v0.0.0-20201001150757-1cec324746b2
github.com/robfig/cron/v3 v3.0.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
github.com/smartystreets/assertions v1.2.0 // indirect github.com/smartystreets/assertions v1.2.0 // indirect
@@ -126,12 +63,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.28.0 // indirect golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.30.0 // indirect golang.org/x/net v0.30.0 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.26.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.19.0 // indirect golang.org/x/text v0.26.0 // indirect
google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.36.6 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

1061
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,135 @@
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(clientID string, username string, cookies ...*http.Cookie) (*Token, error)
Logout(clientID string, token string, cookies ...*http.Cookie) (*Token, error) // Login/Consent Provider endpoints (Hydra redirects here)
Introspect(token string, cookie ...*http.Cookie) (bool, error) // InitiateLogin starts a new OAuth2 flow server-side and returns the login_challenge
Refresh(client_id string, token *Token) (*Token, error) // generated by Hydra. Useful for thick clients that cannot follow browser redirects.
CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) bool InitiateLogin(clientID string, redirectURI string) (string, error)
GetLoginChallenge(challenge string) (*LoginChallenge, error)
AcceptLogin(challenge string, subject string) (*Redirect, error)
RejectLogin(challenge string, reason string) (*Redirect, error)
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)
// Server-side flow completion (for thick clients that cannot follow browser redirects)
// FollowToConsentChallenge follows the redirect_to from AcceptLogin to extract the consent_challenge.
// loginChallenge is used to replay the CSRF cookie set during InitiateLogin.
FollowToConsentChallenge(redirectTo string, loginChallenge string) (string, error)
// ExchangeCodeForToken follows the redirect_to from AcceptConsent, extracts the auth code,
// and exchanges it for a token at Hydra's token endpoint.
// loginChallenge is used to replay the CSRF cookie and is cleaned up after use.
ExchangeCodeForToken(redirectTo string, clientID string, loginChallenge 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) (*claims.Claims, string, 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"`
ExpiresIn int64 `json:"expires_in"` RefreshToken string `json:"refresh_token,omitempty"`
TokenType string `json:"token_type"` IDToken string `json:"id_token,omitempty"`
ExpiresIn int64 `json:"expires_in"`
Username string `json:"username,omitempty"` TokenType string `json:"token_type"`
Password string `json:"password,omitempty"` Scope string `json:"scope,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 {
return a[conf.GetConfig().Auth] for k := range a {
if strings.Contains(conf.GetConfig().Auth, k) {
return a[k]
}
}
return nil
} }

View File

@@ -1,19 +1,19 @@
package auth_connectors package auth_connectors
import ( import (
"encoding/base64" "crypto/rand"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/http/cookiejar"
"net/url" "net/url"
"oc-auth/conf" "oc-auth/conf"
"oc-auth/infrastructure/claims" "oc-auth/infrastructure/claims"
"regexp"
"strconv"
"strings" "strings"
"time" "sync"
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"
@@ -21,17 +21,17 @@ import (
) )
type HydraConnector struct { type HydraConnector struct {
State string `json:"state"` Caller *tools.HTTPCaller
Scopes string `json:"scope"` cookieJars sync.Map // map[loginChallenge] *cookiejar.Jar
ResponseType string `json:"response_type"`
Caller *tools.HTTPCaller
} }
func (a HydraConnector) Status() tools.State { func (h *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().AuthConnectorHost host := conf.GetConfig().AuthConnectPublicHost
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 {
@@ -44,244 +44,513 @@ func (a HydraConnector) Status() tools.State {
return tools.ALIVE return tools.ALIVE
} }
// urlFormat formats the URL of the peer with the data type API function // getPath builds the base URL for Hydra API calls
func (a *HydraConnector) urlFormat(url string, replaceWith string) string { func (h *HydraConnector) getPath(isAdmin bool, isOauth bool) string {
// localhost is replaced by the local peer URL host := conf.GetConfig().AuthConnectPublicHost
// because localhost must collide on a web request security protocol if isAdmin {
r := regexp.MustCompile("(http://[a-z]+:[0-9]+)/oauth2") host = conf.GetConfig().AuthConnectorHost
t := r.FindString(url)
if t != "" {
url = strings.Replace(url, t, replaceWith, -1)
} }
return url if conf.GetConfig().Local {
} 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(client_id string, 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(client_id, token.AccessToken)
if err != nil {
return nil, err
}
return a.Login(client_id, 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(clientID string) 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 ""
}
for _, c := range clients {
if c.(map[string]interface{})["client_name"].(string) == clientID {
return c.(map[string]interface{})["client_id"].(string)
}
}
return clients[0].(map[string]interface{})["client_id"].(string)
}
func (a HydraConnector) Login(clientID string, username string, cookies ...*http.Cookie) (t *Token, err error) {
fmt.Println("login", clientID, username)
clientID = a.getClient(clientID)
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), "", "", []string{}, nil).Search(nil, strconv.Itoa(peer.SELF.EnumIndex()), false)
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(clientID, username, pp.Data[0].(*peer.Peer))
fmt.Println("claims", c.Session.AccessToken)
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(clientID string, token string, cookies ...*http.Cookie) (*Token, error) {
clientID = a.getClient(clientID)
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", clientID)
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) + "/admin" port = fmt.Sprintf("%v", conf.GetConfig().AuthConnectorAdminPort)
} }
oauth := "" oauth := ""
if isOauth { if isOauth {
oauth = "/oauth2" oauth = "/oauth2"
} }
return "http://" + host + ":" + port + oauth return "http://" + host + ":" + port + oauth
} }
func (a HydraConnector) CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) bool { // InitiateLogin starts a new OAuth2 authorization flow with Hydra server-side.
if forward == "" || method == "" { // It calls Hydra's /oauth2/auth endpoint without following the redirect, then extracts
return false // the login_challenge from the Location header. For thick clients that cannot follow
// browser redirects.
func (h *HydraConnector) InitiateLogin(clientID string, redirectURI string) (string, error) {
stateBytes := make([]byte, 16)
if _, err := rand.Read(stateBytes); err != nil {
return "", fmt.Errorf("failed to generate state: %w", err)
} }
var c claims.Claims state := hex.EncodeToString(stateBytes)
token := strings.Split(reqToken, ".")
if len(token) > 2 { params := fmt.Sprintf("client_id=%s&response_type=code&scope=openid&state=%s",
bytes, err := base64.StdEncoding.DecodeString(token[2]) url.QueryEscape(clientID), state)
if err != nil { if redirectURI != "" {
return false params += "&redirect_uri=" + url.QueryEscape(redirectURI)
}
err = json.Unmarshal(bytes, &c)
if err != nil {
return false
}
} }
// ask keto for permission is in claims authURL := h.getPath(false, false) + "/oauth2/auth?" + params
ok, err := claims.GetClaims().DecodeClaimsInToken(host, method, forward, c, publicKey, external)
jar, _ := cookiejar.New(nil)
client := &http.Client{
Jar: jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse // do not follow redirects
},
}
resp, err := client.Get(authURL)
if err != nil { if err != nil {
fmt.Println("Failed to decode claims", err) return "", fmt.Errorf("failed to initiate login with Hydra: %w", err)
} }
return ok defer resp.Body.Close()
location := resp.Header.Get("Location")
if location == "" {
return "", fmt.Errorf("hydra did not return a redirect location (status %d)", resp.StatusCode)
}
parsed, err := url.Parse(location)
if err != nil {
return "", fmt.Errorf("failed to parse redirect location: %w", err)
}
challenge := parsed.Query().Get("login_challenge")
if challenge == "" {
return "", fmt.Errorf("login_challenge not found in redirect location: %s", location)
}
// Save the cookie jar so server-side flow completion can reuse the CSRF cookie
h.cookieJars.Store(challenge, jar)
return challenge, nil
}
// GetLoginChallenge retrieves login challenge details from Hydra admin API
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) (*claims.Claims, string, int) {
if forward == "" || method == "" {
return nil, "", http.StatusUnauthorized
}
// Defense in depth: only SELF peer requests are allowed.
/*if external {
return http.StatusUnauthorized
}*/
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 {
logger.Error().Msg("Forward auth introspect failed: " + err.Error())
}
return nil, "", http.StatusUnauthorized
}
// 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
}
}
// For SELF peer requests skip the signature check (internal traffic).
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, permKey, err := claims.GetClaims().DecodeClaimsInToken(host, method, forward, sessionClaims, publicKey, external)
if err != nil {
logger.Error().Msg("Failed to decode claims in forward auth: " + err.Error())
return nil, "", http.StatusForbidden
}
if !ok {
return nil, "", http.StatusForbidden
}
return &sessionClaims, permKey, http.StatusOK
}
// FollowToConsentChallenge follows the redirect_to returned by AcceptLogin.
// Hydra redirects once to the consent URL — this extracts the consent_challenge from it.
// loginChallenge is used to retrieve the CSRF cookie jar saved during InitiateLogin.
func (h *HydraConnector) FollowToConsentChallenge(redirectTo string, loginChallenge string) (string, error) {
// The redirect_to URL uses the public host (via reverse proxy).
// Rewrite it to hit Hydra directly using its internal address.
internalURL, err := rewriteToInternalHydra(h, redirectTo)
if err != nil {
return "", fmt.Errorf("failed to rewrite redirect URL: %w", err)
}
var jar http.CookieJar
if v, ok := h.cookieJars.Load(loginChallenge); ok {
jar = v.(*cookiejar.Jar)
}
client := &http.Client{
Jar: jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(internalURL)
if err != nil {
return "", fmt.Errorf("failed to follow login redirect: %w", err)
}
defer resp.Body.Close()
location := resp.Header.Get("Location")
if location == "" {
return "", fmt.Errorf("no redirect location after following login redirect (status %d)", resp.StatusCode)
}
parsed, err := url.Parse(location)
if err != nil {
return "", fmt.Errorf("failed to parse consent redirect: %w", err)
}
challenge := parsed.Query().Get("consent_challenge")
if challenge == "" {
return "", fmt.Errorf("consent_challenge not found in redirect: %s", location)
}
return challenge, nil
}
// ExchangeCodeForToken follows the redirect_to returned by AcceptConsent to extract the
// authorization code, then exchanges it for a token at Hydra's token endpoint.
// loginChallenge is used to retrieve the CSRF cookie jar and clean it up after use.
func (h *HydraConnector) ExchangeCodeForToken(redirectTo string, clientID string, loginChallenge string) (*TokenResponse, error) {
internalURL, err := rewriteToInternalHydra(h, redirectTo)
if err != nil {
return nil, fmt.Errorf("failed to rewrite redirect URL: %w", err)
}
var jar http.CookieJar
if v, ok := h.cookieJars.Load(loginChallenge); ok {
jar = v.(*cookiejar.Jar)
}
client := &http.Client{
Jar: jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(internalURL)
if err != nil {
return nil, fmt.Errorf("failed to follow consent redirect: %w", err)
}
defer resp.Body.Close()
location := resp.Header.Get("Location")
if location == "" {
return nil, fmt.Errorf("no redirect after consent (status %d)", resp.StatusCode)
}
parsed, err := url.Parse(location)
if err != nil {
return nil, fmt.Errorf("failed to parse code redirect: %w", err)
}
code := parsed.Query().Get("code")
if code == "" {
return nil, fmt.Errorf("code not found in redirect: %s", location)
}
// Reconstruct redirect_uri without query/fragment — must match the registered value
parsed.RawQuery = ""
parsed.Fragment = ""
redirectURI := parsed.String()
cfg := conf.GetConfig()
vals := url.Values{}
vals.Add("grant_type", "authorization_code")
vals.Add("code", code)
vals.Add("client_id", clientID)
vals.Add("client_secret", cfg.ClientSecret)
vals.Add("redirect_uri", redirectURI)
resp2, err := h.Caller.CallForm(http.MethodPost, h.getPath(false, true), "/token", vals,
"application/x-www-form-urlencoded", true)
if err != nil {
return nil, fmt.Errorf("failed to exchange code for token: %w", err)
}
defer resp2.Body.Close()
b, err := io.ReadAll(resp2.Body)
if err != nil {
return nil, err
}
if resp2.StatusCode >= 300 {
return nil, fmt.Errorf("token exchange failed (%s): %s", resp2.Status, string(b))
}
var result TokenResponse
if err := json.Unmarshal(b, &result); err != nil {
return nil, err
}
// Cookie jar no longer needed — clean up
h.cookieJars.Delete(loginChallenge)
return &result, nil
}
// rewriteToInternalHydra rewrites a public-facing Hydra URL to use the internal Hydra address.
// The redirect_to from Hydra uses the public host/port (possibly behind a reverse proxy),
// but server-side follow-ups must hit Hydra directly.
// It keeps the path suffix after "/oauth2" and the full query string.
func rewriteToInternalHydra(h *HydraConnector, publicURL string) (string, error) {
parsed, err := url.Parse(publicURL)
if err != nil {
return "", fmt.Errorf("invalid redirect URL: %w", err)
}
// Extract the path segment from "/oauth2" onward (e.g. "/oauth2/auth")
const marker = "/oauth2"
idx := strings.Index(parsed.Path, marker)
if idx < 0 {
return "", fmt.Errorf("redirect URL has no /oauth2 path segment: %s", publicURL)
}
suffix := parsed.Path[idx:] // e.g. "/oauth2/auth"
internal := h.getPath(false, false) + suffix
if parsed.RawQuery != "" {
internal += "?" + parsed.RawQuery
}
return internal, nil
}
// 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,6 +12,7 @@ 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"
@@ -48,6 +49,7 @@ 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"`
@@ -64,11 +66,12 @@ 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, IsTLS: cnf.IsTLS}, connector: &ldapConnector{BaseDN: cnf.BaseDN, RoleBaseDN: cnf.RoleBaseDN, UserBaseDN: cnf.UserBaseDN, IsTLS: cnf.IsTLS},
cache: freecache.NewCache(cnf.CacheSize * 1024), cache: freecache.NewCache(cnf.CacheSize * 1024),
} }
} }
@@ -83,27 +86,25 @@ func (cli *Client) Authenticate(ctx context.Context, username string, password s
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 { if err != nil || details == nil {
return false, err return false, err
} }
if details == nil {
return false, nil
}
a := details["dn"] a := details["dn"]
logger.Debug().Msgf("Binding DN: %s", a[0])
if err := cn.Bind(a[0], password); err != nil { if err := cn.Bind(a[0], password); err != nil {
logger.Error().Msg("LDAP bind failed: " + err.Error())
if err == errInvalidCredentials { if err == errInvalidCredentials {
return false, nil return false, nil
} }
@@ -228,7 +229,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) {
panic("You should never see that") return nil, errors.New("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.
@@ -280,13 +281,15 @@ 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 {
fmt.Println("Failed to create a LDAP connection", "address", addr) log := oclib.GetLogger()
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()
fmt.Println("a LDAP connection is cancelled", "address", addr) log := oclib.GetLogger()
log.Debug().Msgf("LDAP connection cancelled: %s", addr)
return return
case ch <- cn: case ch <- cn:
} }
@@ -300,6 +303,8 @@ func (cli *Client) connect(ctx context.Context) <-chan conn {
} }
func (cli *Client) findRoles(cn conn, attrs ...string) (map[string]LDAPRoles, error) { func (cli *Client) findRoles(cn conn, attrs ...string) (map[string]LDAPRoles, error) {
logger := oclib.GetLogger()
logger.Debug().Msg("Finding LDAP roles")
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 {
@@ -307,7 +312,7 @@ func (cli *Client) findRoles(cn conn, attrs ...string) (map[string]LDAPRoles, er
} }
} }
entries, err := cn.SearchRoles(attrs...) entries, err := cn.SearchRoles(attrs...)
fmt.Println("entries", entries) logger.Debug().Msgf("Found %d LDAP role entries", len(entries))
if err != nil { if err != nil {
return map[string]LDAPRoles{}, err return map[string]LDAPRoles{}, err
} }
@@ -322,7 +327,7 @@ func (cli *Client) findRoles(cn conn, attrs ...string) (map[string]LDAPRoles, er
// 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) {
panic("You should never see that") return nil, errors.New("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.
@@ -340,7 +345,7 @@ func (cli *Client) findRoles(cn conn, attrs ...string) (map[string]LDAPRoles, er
if claims[appID].Members[role] == nil { if claims[appID].Members[role] == nil {
claims[appID].Members[role] = []string{} claims[appID].Members[role] = []string{}
} }
fmt.Println("entry", entry) logger.Debug().Msgf("Processing role entry: %v", entry["dn"])
memberDNs, ok := entry["member"] memberDNs, ok := entry["member"]
for _, memberDN := range memberDNs { for _, memberDN := range memberDNs {
if !ok || memberDN == "" { if !ok || memberDN == "" {
@@ -372,19 +377,21 @@ func (cli *Client) findRoles(cn conn, attrs ...string) (map[string]LDAPRoles, er
// 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][]string, 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) != 1 { if len(entries) == 0 {
// 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
} }
@@ -403,6 +410,7 @@ 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
} }
@@ -424,12 +432,13 @@ 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, RoleBaseDN: c.RoleBaseDN}, nil return &ldapConn{Conn: ldapcn, BaseDN: c.BaseDN, UserBaseDN: c.UserBaseDN, RoleBaseDN: c.RoleBaseDN}, nil
} }
type ldapConn struct { type ldapConn struct {
*ldap.Conn *ldap.Conn
BaseDN string BaseDN string
UserBaseDN string
RoleBaseDN string RoleBaseDN string
} }
@@ -445,7 +454,7 @@ func (c *ldapConn) SearchUser(user string, attrs ...string) ([]map[string][]stri
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.BaseDN, query, attrs) return c.searchEntries(c.UserBaseDN, query, attrs)
} }
func (c *ldapConn) SearchUserRoles(user string, attrs ...string) ([]map[string][]string, error) { func (c *ldapConn) SearchUserRoles(user string, attrs ...string) ([]map[string][]string, error) {
@@ -463,15 +472,18 @@ func (c *ldapConn) SearchRoles(attrs ...string) ([]map[string][]string, error) {
// 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][]string, 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][]string
for _, v := range res.Entries { for _, v := range res.Entries {
entry := map[string][]string{"dn": []string{v.DN}} entry := map[string][]string{"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

View File

@@ -1,24 +1,32 @@
package claims package claims
import ( import (
"fmt"
"oc-auth/conf" "oc-auth/conf"
"reflect"
"strings"
"cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/models/peer"
"github.com/google/go-cmp/cmp"
) )
// Tokenizer interface // ClaimService builds and verifies OAuth2 session claims
type ClaimService interface { type ClaimService interface {
AddClaimsToToken(clientID string, userId string, peer *peer.Peer) Claims // BuildConsentSession builds the session payload for Hydra consent accept.
DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, error) // 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, string, error)
} }
// SessionClaims struct // SessionClaims contains access_token and id_token claim maps
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 struct // Claims is the top-level session structure passed to Hydra consent accept
type Claims struct { type Claims struct {
Session SessionClaims `json:"session"` Session SessionClaims `json:"session"`
} }
@@ -27,6 +35,93 @@ var t = map[string]ClaimService{
"hydra": HydraClaims{}, "hydra": HydraClaims{},
} }
func GetClaims() ClaimService { func cleanMap(m map[string]interface{}) map[string]interface{} {
return t[conf.GetConfig().Auth] if m == nil {
return map[string]interface{}{}
}
ignored := map[string]bool{
"exp": true,
"iat": true,
"nbf": true,
}
out := make(map[string]interface{})
for k, v := range m {
if ignored[k] {
continue
}
switch val := v.(type) {
case map[string]interface{}:
out[k] = cleanMap(val)
default:
out[k] = val
}
}
return out
}
func (c *Claims) EqualExt(ext map[string]interface{}) bool {
claims := &Claims{}
claims.SessionFromExt(ext)
return c.EqualClaims(claims)
}
func (c *Claims) EqualClaims(claims *Claims, permsKey ...string) bool {
c.normalizeClaims()
claims.normalizeClaims()
if len(permsKey) > 0 {
for _, p := range permsKey {
if !(claims.Session.AccessToken[p] != nil && c.Session.AccessToken[p] != nil && claims.Session.AccessToken[p] == c.Session.AccessToken[p]) {
return false
}
}
return true
}
ok := reflect.DeepEqual(c.Session, claims.Session)
if !ok {
fmt.Println(cmp.Diff(c.Session, claims.Session))
}
return ok
}
func (c *Claims) normalizeClaims() {
c.Session.AccessToken = cleanMap(c.Session.AccessToken)
c.Session.IDToken = cleanMap(c.Session.IDToken)
}
func (c *Claims) SessionFromExt(ext map[string]interface{}) {
var access map[string]interface{}
var id map[string]interface{}
if v, ok := ext["access_token"].(map[string]interface{}); ok && v != nil {
access = v
} else {
access = map[string]interface{}{}
}
if v, ok := ext["id_token"].(map[string]interface{}); ok && v != nil {
id = v
} else {
id = map[string]interface{}{}
}
c.Session = SessionClaims{
AccessToken: access,
IDToken: id,
}
}
func GetClaims() ClaimService {
for k := range t {
if strings.Contains(conf.GetConfig().Auth, k) {
return t[k]
}
}
return nil
} }

View File

@@ -4,14 +4,13 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt"
"oc-auth/conf" "oc-auth/conf"
"oc-auth/infrastructure/perms_connectors" "oc-auth/infrastructure/perms_connectors"
"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"
) )
@@ -27,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
} }
// decode key expect to extract method and path from key // decodeKey extracts method and path from a permission 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 {
@@ -46,7 +45,10 @@ 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)) // get public key into a variable spkiBlock, _ := pem.Decode([]byte(publicKey))
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
@@ -56,18 +58,19 @@ 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 != "" {
@@ -77,32 +80,35 @@ func (h HydraClaims) clearBlank(path []string) []string {
return newPath return newPath
} }
func (a HydraClaims) CheckExpiry(exp int64) bool { // DecodeClaimsInToken verifies permissions from claims in a standard JWT (via introspection)
now := time.Now().UTC().Unix() func (h HydraClaims) DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, string, error) {
return now <= exp logger := oclib.GetLogger()
}
func (h HydraClaims) DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, error) {
idTokenClaims := sessionClaims.Session.IDToken idTokenClaims := sessionClaims.Session.IDToken
if idTokenClaims["signature"] == nil {
return false, errors.New("no signature found") // Signature verification: skip if signature is empty (internal requests)
} if sig, ok := idTokenClaims["signature"].(string); ok && sig != "" {
signature := idTokenClaims["signature"].(string) if ok, err := h.DecodeSignature(host, sig, publicKey); !ok {
if ok, err := h.DecodeSignature(host, signature, publicKey); !ok { return false, "", err
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 {
match := true pStr, ok := p.(string)
splittedP := h.clearBlank(strings.Split(p.(string), "/")) if !ok {
continue
}
splittedP := h.clearBlank(strings.Split(pStr, "/"))
if len(splittedP) != len(splittedPath) { if len(splittedP) != len(splittedPath) {
continue continue
} }
match := true
for i, v := range splittedP { for i, v := range splittedP {
if strings.Contains(v, ":") { // is a param if strings.Contains(v, ":") { // is a param
continue continue
@@ -118,45 +124,64 @@ 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: p.(string), Object: pStr,
} }
return perms_connectors.GetPermissionConnector("").CheckPermission(perm, nil, true), nil return perms_connectors.GetPermissionConnector("").CheckPermission(perm, nil, true), m, nil
} }
} }
return false, errors.New("no permission found") logger.Error().Msg("No permission found for " + method + " " + forward)
return false, "", errors.New("no permission found")
} }
// add claims to token method of HydraTokenizer // BuildConsentSession builds the session payload for Hydra consent accept.
func (h HydraClaims) AddClaimsToToken(clientID string, userId string, p *peer.Peer) Claims { // Claims are injected into the Hydra JWT — not appended to the token as before.
claims := Claims{} func (h HydraClaims) BuildConsentSession(clientID string, userId string, p *peer.Peer) 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 {
return claims logger.Error().Msg("Failed to get permissions for user " + userId + ": " + err.Error())
return c
} }
claims.Session.AccessToken = make(map[string]interface{})
claims.Session.IDToken = make(map[string]interface{}) c.Session.AccessToken = make(map[string]interface{})
fmt.Println("PERMS err 1", perms, err) 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
} }
claims.Session.AccessToken[key] = perm.Subject c.Session.AccessToken[key] = perm.Subject
} }
sign, err := h.encodeSignature(p.Url)
sign, err := h.encodeSignature(p.APIUrl)
if err != nil { if err != nil {
return claims logger.Error().Msg("Failed to encode signature: " + err.Error())
return c
} }
claims.Session.IDToken["username"] = userId
claims.Session.IDToken["peer_id"] = p.UUID c.Session.AccessToken["peer_id"] = p.UUID
// we should get group from user c.Session.AccessToken["user_id"] = userId
c.Session.IDToken["user_id"] = userId
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 {
return claims logger.Error().Msg("Failed to get groups for user " + userId + ": " + err.Error())
return c
} }
claims.Session.IDToken["client_id"] = clientID c.Session.AccessToken["groups"] = groups
claims.Session.IDToken["groups"] = groups c.Session.IDToken["groups"] = groups
claims.Session.IDToken["signature"] = sign
return claims roles, err := perms_connectors.KetoConnector{}.GetRoleByUser(userId)
if err != nil {
logger.Error().Msg("Failed to get roles for user " + userId + ": " + err.Error())
return c
}
c.Session.IDToken["roles"] = roles
c.Session.IDToken["signature"] = sign
return c
} }

View File

@@ -6,6 +6,7 @@ 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"
@@ -56,7 +57,10 @@ 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().PermissionConnectorHost host := conf.GetConfig().PermissionConnectorReadHost
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 {
@@ -78,7 +82,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(err.Error()) log.Error().Msg("CheckPermission " + err.Error())
return false return false
} }
return len(perms) > 0 return len(perms) > 0
@@ -125,8 +129,12 @@ func (k KetoConnector) CreatePermission(permID string, relation string, internal
if err != nil { if err != nil {
return "", 422, err return "", 422, err
} }
k.BindPermission("admin", permID, "permits"+meth.String()) id, code, err := k.creates(permID, "permits"+meth.String(), k.scope())
return k.creates(permID, "permits"+meth.String(), k.scope()) if err != nil && !strings.Contains(err.Error(), "already exist") {
return id, code, err
}
k.BindPermission(conf.GetConfig().AdminRole, permID, "permits"+meth.String())
return id, code, nil
} }
func (k KetoConnector) creates(object string, relation string, subject string) (string, int, error) { func (k KetoConnector) creates(object string, relation string, subject string) (string, int, error) {
@@ -194,7 +202,8 @@ 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)
fmt.Println("ROLES", roles, err) 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
} }
@@ -217,7 +226,10 @@ 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().PermissionConnectorHost host := conf.GetConfig().PermissionConnectorReadHost
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))
@@ -250,7 +262,8 @@ 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) {
fmt.Println("BIND ROLE", userID, roleID) 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)
} }
@@ -344,32 +357,38 @@ func (k KetoConnector) createRelationShip(object string, relation string, subjec
} }
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": k.namespace(), "object": s.Object, "relation": s.Relation, "subject_id": s.Subject}
} }
host := conf.GetConfig().PermissionConnectorHost host := conf.GetConfig().PermissionConnectorWriteHost
if conf.GetConfig().Local {
host = "localhost"
}
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(err.Error()) log.Error().Msg("createRelationShip" + err.Error())
return nil, 500, err return nil, 500, err
} }
var data map[string]interface{} 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().Msg(err.Error()) log.Error().Msgf("createRelationShip unmarshal error: %s, err=%v", string(b), err)
return nil, 500, err return nil, 500, err
} }
perm := &Permission{ perm := &Permission{}
Object: data["object"].(string), if data != nil {
Relation: data["relation"].(string), perm = &Permission{
Subject: data["subject_id"].(string), Object: data["object"].(string),
} Relation: data["relation"].(string),
if data["subject_set"] != nil { Subject: data["subject_id"].(string),
sub := data["subject_set"].(map[string]interface{}) }
perm.SubPermission = &Permission{ if data["subject_set"] != nil {
Object: sub["object"].(string), sub := data["subject_set"].(map[string]interface{})
Relation: sub["relation"].(string), perm.SubPermission = &Permission{
Subject: sub["subject_id"].(string), Object: sub["object"].(string),
Relation: sub["relation"].(string),
Subject: sub["subject_id"].(string),
}
} }
} }
return perm, 200, nil return perm, 200, nil
@@ -382,12 +401,15 @@ 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().PermissionConnectorHost host := conf.GetConfig().PermissionConnectorWriteHost
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(err.Error()) log.Error().Msg("deleteRelationShip " + err.Error())
return nil, 500, err return nil, 500, err
} }
var data map[string]interface{} var data map[string]interface{}

View File

@@ -2,6 +2,7 @@ package perms_connectors
import ( import (
"oc-auth/conf" "oc-auth/conf"
"strings"
"cloud.o-forge.io/core/oc-lib/tools" "cloud.o-forge.io/core/oc-lib/tools"
) )
@@ -55,5 +56,10 @@ var c = map[string]PermConnector{
} }
func GetPermissionConnector(scope string) PermConnector { func GetPermissionConnector(scope string) PermConnector {
return c[conf.GetConfig().PermissionConnectorHost] for k := range c {
if strings.Contains(conf.GetConfig().PermissionConnectorReadHost, k) {
return c[k]
}
}
return nil
} }

View File

@@ -1,21 +0,0 @@
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

View File

@@ -1,18 +0,0 @@
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

@@ -1,78 +0,0 @@
version: "3"
services:
hydra-client-2:
image: oryd/hydra:v2.2.0
container_name: hydra-client-2
environment:
HYDRA_ADMIN_URL: http://hydra-2:4445
ORY_SDK_URL: http://hydra-2: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-2
healthcheck:
test: ["CMD", "curl", "-f", "http://hydra-2:4445"]
interval: 10s
timeout: 10s
retries: 10
hydra-2:
container_name: hydra-2
image: oryd/hydra:v2.2.0
environment:
SECRETS_SYSTEM: oc-auth-got-secret
LOG_LEAK_SENSITIVE_VALUES: true
URLS_SELF_ISSUER: http://hydra-2:4444
URLS_SELF_PUBLIC: http://hydra-2: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:
- "4446:4444"
- "4447:4445"
deploy:
restart_policy:
condition: on-failure
ldap-2:
image: pgarrett/ldap-alpine
container_name: ldap-2
volumes:
- "./ldap-2.ldif:/ldif/ldap.ldif"
networks:
- hydra-net
- catalog
ports:
- "389:389"
deploy:
restart_policy:
condition: on-failure
networks:
hydra-net:
catalog:
external: true

View File

@@ -1,79 +0,0 @@
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

View File

@@ -1,24 +0,0 @@
dn: uid=admin2,ou=Users,dc=example,dc=com
objectClass: inetOrgPerson
cn: Admin2
sn: Istrator
uid: admin2
userPassword: admin2
mail: admin2@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=admin2,ou=Users,dc=example,dc=com

View File

@@ -1,24 +0,0 @@
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

172
main.go
View File

@@ -2,19 +2,15 @@ package main
import ( import (
"context" "context"
"errors" "encoding/json"
"fmt"
"oc-auth/conf" "oc-auth/conf"
"oc-auth/infrastructure" "oc-auth/infrastructure"
auth_connectors "oc-auth/infrastructure/auth_connector" auth_connectors "oc-auth/infrastructure/auth_connector"
_ "oc-auth/routers" _ "oc-auth/routers"
"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"
) )
@@ -26,120 +22,122 @@ 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() {
// Init the oc-lib oclib.InitAPI(appname)
oclib.Init(appname)
// Load the right config file // Load the right config file
o := oclib.GetConfLoader() o := oclib.GetConfLoader(appname)
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.GetIntDefault("AUTH_CONNECTOR_ADMIN_PORT", 4445) conf.GetConfig().AuthConnectorAdminPort = o.GetStringDefault("AUTH_CONNECTOR_ADMIN_PORT", "4445/admin")
conf.GetConfig().PermissionConnectorHost = o.GetStringDefault("PERMISSION_CONNECTOR_HOST", "keto") conf.GetConfig().PermissionConnectorWriteHost = o.GetStringDefault("PERMISSION_CONNECTOR_WRITE_HOST", "keto")
conf.GetConfig().PermissionConnectorPort = o.GetIntDefault("PERMISSION_CONNECTOR_PORT", 4466) conf.GetConfig().PermissionConnectorReadHost = o.GetStringDefault("PERMISSION_CONNECTOR_READ_HOST", "keto")
conf.GetConfig().PermissionConnectorAdminPort = o.GetIntDefault("PERMISSION_CONNECTOR_ADMIN_PORT", 4467) conf.GetConfig().PermissionConnectorPort = o.GetStringDefault("PERMISSION_CONNECTOR_PORT", "4466")
conf.GetConfig().PermissionConnectorAdminPort = o.GetStringDefault("PERMISSION_CONNECTOR_ADMIN_PORT", "4467")
// config LDAP conf.GetConfig().Origin = o.GetStringDefault("ADMIN_ORIGIN", "http://localhost:8000")
conf.GetConfig().AdminOrigin = o.GetStringDefault("ADMIN_ORIGIN", "http://localhost:8001")
conf.GetConfig().OAuth2ClientID = o.GetStringDefault("OAUTH2_CLIENT_ID", "oc-auth")
conf.GetConfig().OAuth2AdminClientID = o.GetStringDefault("OAUTH2_ADMIN_CLIENT_ID", "oc-auth-admin")
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().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")
err := generateSelfPeer() go generateRole()
if err != nil { go discovery()
panic(err)
}
generateRole()
discovery()
beego.Run() beego.Run()
} }
func generateRole() { func generateRole() {
logger := oclib.GetLogger()
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
fmt.Println("Recovered in f", r) logger.Error().Msgf("generateRole recovered from panic: %v", r)
} }
}() }()
// if from ldap, create roles from ldap
if conf.GetConfig().SourceMode == "ldap" { if conf.GetConfig().SourceMode == "ldap" {
ldap := auth_connectors.New() for {
roles, err := ldap.GetRoles(context.Background()) ldap := auth_connectors.New()
if err != nil { roles, err := ldap.GetRoles(context.Background())
panic(err) if err == nil {
} logger.Info().Msgf("Syncing %d LDAP role groups to Keto", len(roles))
fmt.Println("ROLE", roles) for _, role := range roles {
for _, role := range roles { for r, m := range role.Members {
for r, m := range role.Members { infrastructure.GetPermissionConnector("").CreateRole(r)
infrastructure.GetPermissionConnector("").CreateRole(r) for _, p := range m {
for _, p := range m { infrastructure.GetPermissionConnector("").BindRole(r, p)
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
} }
} }
} }
} }
func generateSelfPeer() error {
// TODO check if files at private & public path are set
// 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 _, 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 := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), "", "", []string{}, nil).Search(nil, strconv.Itoa(peer.SELF.EnumIndex()), false)
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 := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), "", "", []string{}, nil).StoreOne(peer.Serialize(peer))
if data.Err != "" {
return errors.New(data.Err)
}
return nil
}
func discovery() { func discovery() {
api := tools.API{} logger := oclib.GetLogger()
conn := infrastructure.GetPermissionConnector("") defer func() {
if r := recover(); r != nil {
conn.CreateRole(conf.GetConfig().AdminRole) logger.Error().Msgf("discovery recovered from panic: %v", r)
conn.BindRole(conf.GetConfig().AdminRole, "admin") }
addPermissions := func(m map[string]interface{}) { }()
for k, v := range m { for {
for _, p := range v.([]interface{}) { api := tools.API{}
conn.CreatePermission(k, p.(string), true) conn := infrastructure.GetPermissionConnector("")
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)
} }
} }
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 {
conn.DeletePermission(k, p.(string), true)
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

@@ -81,7 +81,16 @@ 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: "InternaisDraftlAuthForward", 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.ControllerComments{
Method: "InternalAuthForward",
Router: `/forward`, Router: `/forward`,
AllowHTTPMethods: []string{"get"}, AllowHTTPMethods: []string{"get"},
MethodParams: param.Make(), MethodParams: param.Make(),
@@ -97,6 +106,15 @@ 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: "GetLogin",
Router: `/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.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"],
beego.ControllerComments{ beego.ControllerComments{
Method: "Login", Method: "Login",
@@ -106,6 +124,15 @@ 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: "GetLogout",
Router: `/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.GlobalControllerRouter["oc-auth/controllers:OAuthController"] = append(beego.GlobalControllerRouter["oc-auth/controllers:OAuthController"],
beego.ControllerComments{ beego.ControllerComments{
Method: "LogOut", Method: "LogOut",

View File

@@ -15,18 +15,57 @@
}, },
"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": "auth forward\n\u003cbr\u003e", "description": "Forward auth for Traefik — validates JWT via Hydra introspection.\n\u003cbr\u003e",
"operationId": "OAuthController.AuthForward", "operationId": "OAuthController.AuthForward",
"parameters": [ "parameters": [
{ {
"in": "header", "in": "header",
"name": "Authorization", "name": "Authorization",
"description": "auth token", "description": "Bearer token",
"type": "string" "type": "string"
} }
], ],
@@ -216,80 +255,169 @@
"tags": [ "tags": [
"oc-auth/controllersOAuthController" "oc-auth/controllersOAuthController"
], ],
"description": "introspect token\n\u003cbr\u003e", "description": "Introspect a token — respects Hydra's response\n\u003cbr\u003e",
"operationId": "OAuthController.Introspection", "operationId": "OAuthController.Introspect",
"parameters": [ "parameters": [
{ {
"in": "header", "in": "header",
"name": "Authorization", "name": "Authorization",
"description": "auth token", "description": "Bearer token",
"type": "string" "type": "string"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "{string}" "description": "",
"schema": {
"$ref": "#/definitions/auth_connectors.IntrospectResult"
}
} }
} }
} }
}, },
"/login": { "/login": {
"post": { "get": {
"tags": [ "tags": [
"oc-auth/controllersOAuthController" "oc-auth/controllersOAuthController"
], ],
"description": "authenticate user\n\u003cbr\u003e", "description": "Hydra redirects here with a login_challenge. Returns challenge info or auto-accepts if session exists.\n\u003cbr\u003e",
"operationId": "OAuthController.Login", "operationId": "OAuthController.GetLogin",
"parameters": [ "parameters": [
{ {
"in": "body", "in": "query",
"name": "body", "name": "login_challenge",
"description": "The workflow content", "description": "The login challenge from Hydra",
"required": true, "required": true,
"schema": { "type": "string"
"$ref": "#/definitions/models.workflow"
}
}, },
{ {
"in": "query", "in": "query",
"name": "client_id", "name": "redirect",
"description": "the client_id you want to get", "description": "explicit redirect by passed",
"required": true, "required": true,
"type": "string" "type": "string"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "{string}" "description": "",
"schema": {
"$ref": "#/definitions/auth_connectors.LoginChallenge"
}
},
"400": {
"description": "missing login_challenge"
},
"500": {
"description": "internal error"
}
}
},
"post": {
"tags": [
"oc-auth/controllersOAuthController"
],
"description": "Authenticate user via LDAP and accept Hydra login challenge\n\u003cbr\u003e",
"operationId": "OAuthController.Login",
"parameters": [
{
"in": "query",
"name": "redirect",
"description": "explicit redirect by passed",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "body",
"description": "Login credentials and challenge",
"required": true,
"schema": {
"$ref": "#/definitions/auth_connectors.LoginRequest"
}
}
],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/auth_connectors.Redirect"
}
},
"401": {
"description": "invalid credentials"
},
"500": {
"description": "internal error"
} }
} }
} }
}, },
"/logout": { "/logout": {
"delete": { "get": {
"tags": [ "tags": [
"oc-auth/controllersOAuthController" "oc-auth/controllersOAuthController"
], ],
"description": "unauthenticate user\n\u003cbr\u003e", "description": "Hydra redirects here with a logout_challenge. Accepts the challenge and returns a redirect URL.\n\u003cbr\u003e",
"operationId": "OAuthController.Logout", "operationId": "OAuthController.GetLogout",
"parameters": [ "parameters": [
{ {
"in": "header", "in": "query",
"name": "Authorization", "name": "logout_challenge",
"description": "auth token", "description": "The logout challenge from Hydra",
"required": true,
"type": "string" "type": "string"
}, },
{ {
"in": "query", "in": "query",
"name": "client_id", "name": "redirect",
"description": "the client_id you want to get", "description": "explicit redirect by passed",
"required": true, "required": true,
"type": "string" "type": "string"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "{string}" "description": "",
"schema": {
"$ref": "#/definitions/auth_connectors.Redirect"
}
},
"400": {
"description": "missing logout_challenge"
},
"500": {
"description": "internal error"
}
}
},
"delete": {
"tags": [
"oc-auth/controllersOAuthController"
],
"description": "Revoke an OAuth2 token\n\u003cbr\u003e",
"operationId": "OAuthController.Logout",
"parameters": [
{
"in": "header",
"name": "Authorization",
"description": "Bearer token",
"type": "string"
},
{
"in": "query",
"name": "client_id",
"description": "The client_id",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/auth_connectors.Token"
}
} }
} }
} }
@@ -468,29 +596,28 @@
"tags": [ "tags": [
"oc-auth/controllersOAuthController" "oc-auth/controllersOAuthController"
], ],
"description": "introspect token\n\u003cbr\u003e", "description": "Exchange a refresh_token for a new token set\n\u003cbr\u003e",
"operationId": "OAuthController.Introspection", "operationId": "OAuthController.Refresh",
"parameters": [ "parameters": [
{ {
"in": "body", "in": "body",
"name": "body", "name": "body",
"description": "The token info", "description": "refresh_token and client_id",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/models.Token" "$ref": "#/definitions/object"
} }
},
{
"in": "query",
"name": "client_id",
"description": "the client_id you want to get",
"required": true,
"type": "string"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "{string}" "description": "",
"schema": {
"$ref": "#/definitions/auth_connectors.TokenResponse"
}
},
"401": {
"description": "invalid refresh token"
} }
} }
} }
@@ -699,19 +826,152 @@
} }
}, },
"definitions": { "definitions": {
"models.Token": { "2432.0xc0004a0630.false": {
"title": "Token", "title": "false",
"type": "object" "type": "object"
}, },
"models.workflow": { "4171.0xc0004a0810.false": {
"title": "workflow", "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",
"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": {
"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": "Operations about auth\n" "description": "OAuthController handles OAuth2 login/consent provider endpoints\n"
}, },
{ {
"name": "group", "name": "group",

View File

@@ -12,18 +12,46 @@ 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: |-
auth forward Forward auth for Traefik validates JWT via Hydra introspection.
<br> <br>
operationId: OAuthController.AuthForward operationId: OAuthController.AuthForward
parameters: parameters:
- in: header - in: header
name: Authorization name: Authorization
description: auth token description: Bearer token
type: string type: string
responses: responses:
"200": "200":
@@ -164,61 +192,125 @@ paths:
tags: tags:
- oc-auth/controllersOAuthController - oc-auth/controllersOAuthController
description: |- description: |-
introspect token Introspect a token respects Hydra's response
<br> <br>
operationId: OAuthController.Introspection operationId: OAuthController.Introspect
parameters: parameters:
- in: header - in: header
name: Authorization name: Authorization
description: auth token description: Bearer token
type: string type: string
responses: responses:
"200": "200":
description: '{string}' description: ""
schema:
$ref: '#/definitions/auth_connectors.IntrospectResult'
/login: /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 Authenticate user via LDAP and accept Hydra login challenge
<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: The workflow content description: Login credentials and challenge
required: true required: true
schema: schema:
$ref: '#/definitions/models.workflow' $ref: '#/definitions/auth_connectors.LoginRequest'
responses:
"200":
description: ""
schema:
$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 - in: query
name: client_id name: logout_challenge
description: the client_id you want to get description: The logout challenge from Hydra
required: true
type: string
- in: query
name: redirect
description: explicit redirect by passed
required: true required: true
type: string type: string
responses: responses:
"200": "200":
description: '{string}' description: ""
/logout: 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: |-
unauthenticate user Revoke an OAuth2 token
<br> <br>
operationId: OAuthController.Logout operationId: OAuthController.Logout
parameters: parameters:
- in: header - in: header
name: Authorization name: Authorization
description: auth token description: Bearer token
type: string type: string
- in: query - in: query
name: client_id name: client_id
description: the client_id you want to get description: The client_id
required: true required: true
type: string type: string
responses: responses:
"200": "200":
description: '{string}' description: ""
schema:
$ref: '#/definitions/auth_connectors.Token'
/permission/: /permission/:
get: get:
tags: tags:
@@ -350,24 +442,23 @@ paths:
tags: tags:
- oc-auth/controllersOAuthController - oc-auth/controllersOAuthController
description: |- description: |-
introspect token Exchange a refresh_token for a new token set
<br> <br>
operationId: OAuthController.Introspection operationId: OAuthController.Refresh
parameters: parameters:
- in: body - in: body
name: body name: body
description: The token info description: refresh_token and client_id
required: true required: true
schema: schema:
$ref: '#/definitions/models.Token' $ref: '#/definitions/object'
- in: query
name: client_id
description: the client_id you want to get
required: true
type: string
responses: responses:
"200": "200":
description: '{string}' description: ""
schema:
$ref: '#/definitions/auth_connectors.TokenResponse'
"401":
description: invalid refresh token
/role/: /role/:
get: get:
tags: tags:
@@ -522,16 +613,106 @@ paths:
"200": "200":
description: "" description: ""
definitions: definitions:
models.Token: 2432.0xc0004a0630.false:
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
models.workflow: properties:
title: workflow 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:
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: |
Operations about auth OAuthController handles OAuth2 login/consent provider endpoints
- name: group - name: group
description: | description: |
Operations about auth Operations about auth