60 Commits

Author SHA1 Message Date
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
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 1750 additions and 2045 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 WORKDIR /app
ARG NAME=local COPY go.mod go.sum ./
RUN sed -i '/replace/d' go.mod
RUN cat go.mod
RUN go mod download
WORKDIR /app #----------------------------------------------------------------------------------------------
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,25 @@ 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
Local bool
} }
var instance *Config var instance *Config

View File

@@ -9,124 +9,367 @@ import (
"oc-auth/infrastructure" "oc-auth/infrastructure"
auth_connectors "oc-auth/infrastructure/auth_connector" auth_connectors "oc-auth/infrastructure/auth_connector"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time"
oclib "cloud.o-forge.io/core/oc-lib" oclib "cloud.o-forge.io/core/oc-lib"
"cloud.o-forge.io/core/oc-lib/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 // 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" // @Success 200 {object} auth_connectors.LoginChallenge
// @Success 200 {string} // @Failure 400 missing login_challenge
// @router /logout [delete] // @Failure 500 internal error
func (o *OAuthController) LogOut() { // @router /login [get]
// authorize user func (o *OAuthController) GetLogin() {
clientID := o.Ctx.Input.Query("client_id") logger := oclib.GetLogger()
reqToken := o.Ctx.Request.Header.Get("Authorization") challenge := o.Ctx.Input.Query("login_challenge")
splitToken := strings.Split(reqToken, "Bearer ") if challenge == "" {
if len(splitToken) < 2 { o.Ctx.ResponseWriter.WriteHeader(400)
reqToken = "" o.Data["json"] = map[string]string{"error": "missing login_challenge parameter"}
} else { o.ServeJSON()
reqToken = splitToken[1] return
} }
var res auth_connectors.Token
json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &res)
token, err := infrastructure.GetAuthConnector().Logout(clientID, reqToken) if conf.GetConfig().Local {
if err != nil || token == nil { // In local mode, return a mock challenge for dev
o.Data["json"] = err o.Data["json"] = &auth_connectors.LoginChallenge{
} else { Skip: false,
o.Data["json"] = token Challenge: challenge,
}
o.ServeJSON()
return
} }
o.ServeJSON()
}
// @Title Login loginChallenge, err := infrastructure.GetAuthConnector().GetLoginChallenge(challenge)
// @Description authenticate user if err != nil {
// @Param body body models.workflow true "The workflow content" logger.Error().Msg("Failed to get login challenge: " + err.Error())
// @Param client_id query string true "the client_id you want to get" o.Ctx.ResponseWriter.WriteHeader(500)
// @Success 200 {string} o.Data["json"] = map[string]string{"error": err.Error()}
// @router /login [post] o.ServeJSON()
func (o *OAuthController) Login() { return
// authorize user }
fmt.Println("Login", o.Ctx.Input.Query("client_id"), o.Ctx.Input.Param(":client_id"))
clientID := o.Ctx.Input.Query("client_id") // If skip is true, the user already has an active session — auto-accept
var res auth_connectors.Token if loginChallenge.Skip {
json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &res) redirect, err := infrastructure.GetAuthConnector().AcceptLogin(challenge, loginChallenge.Subject)
if conf.GetConfig().SourceMode == "ldap" { if err != nil {
ldap := auth_connectors.New() logger.Error().Msg("Failed to auto-accept login: " + err.Error())
found, err := ldap.Authenticate(o.Ctx.Request.Context(), res.Username, res.Password) o.Ctx.ResponseWriter.WriteHeader(500)
fmt.Println("found", found, "err", err) o.Data["json"] = map[string]string{"error": err.Error()}
if err != nil || !found {
o.Data["json"] = err
o.Ctx.ResponseWriter.WriteHeader(401)
o.ServeJSON() o.ServeJSON()
return return
} }
o.Data["json"] = redirect
o.ServeJSON()
return
} }
token, err := infrastructure.GetAuthConnector().Login(
clientID, res.Username, // Return challenge info so frontend can render login form
&http.Cookie{ // open a session o.Data["json"] = loginChallenge
Name: "csrf_token", o.ServeJSON()
Value: o.XSRFToken(), }
})
fmt.Println("token", token, "err", err) // @Title PostLogin
if err != nil || token == nil { // @Description Authenticate user via LDAP and accept Hydra login challenge
o.Data["json"] = err // @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()
var req auth_connectors.LoginRequest
if err := json.Unmarshal(o.Ctx.Input.CopyBody(10000000), &req); err != nil {
o.Ctx.ResponseWriter.WriteHeader(400)
o.Data["json"] = map[string]string{"error": "invalid request body"}
o.ServeJSON()
return
}
if req.Username == "" || req.Password == "" {
o.Ctx.ResponseWriter.WriteHeader(400)
o.Data["json"] = map[string]string{"error": "username and password are required"}
o.ServeJSON()
return
}
// Authenticate via LDAP
ldap := auth_connectors.New()
found, err := ldap.Authenticate(o.Ctx.Request.Context(), req.Username, req.Password)
if err != nil || !found {
logger.Error().Msg("LDAP authentication failed for user: " + req.Username)
o.Ctx.ResponseWriter.WriteHeader(401) o.Ctx.ResponseWriter.WriteHeader(401)
o.Data["json"] = map[string]string{"error": "invalid credentials"}
o.ServeJSON()
return
}
if conf.GetConfig().Local {
// In local mode, return a mock token for dev
t := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), "", "", []string{}, nil).Search(
nil, fmt.Sprintf("%v", model.SELF.EnumIndex()), false)
if t.Err == "" && len(t.Data) > 0 {
p := t.Data[0].(*model.Peer)
c := infrastructure.GetClaims().BuildConsentSession("local", req.Username, p)
now := time.Now().UTC()
now = now.Add(3600 * time.Second)
c.Session.AccessToken["exp"] = now.Unix()
b, _ := json.Marshal(c)
token := &auth_connectors.Token{
Active: true,
TokenType: "Bearer",
ExpiresIn: 3600,
AccessToken: "localtoken." + base64.StdEncoding.EncodeToString(b),
}
o.Data["json"] = token
} else {
o.Ctx.ResponseWriter.WriteHeader(401)
o.Data["json"] = map[string]string{"error": "peer not found"}
}
o.ServeJSON()
return
}
if req.LoginChallenge == "" {
o.Ctx.ResponseWriter.WriteHeader(400)
o.Data["json"] = map[string]string{"error": "login_challenge is required in non-local mode"}
o.ServeJSON()
return
}
// Accept the login challenge with Hydra
redirect, err := infrastructure.GetAuthConnector().AcceptLogin(req.LoginChallenge, req.Username)
if err != nil {
logger.Error().Msg("Failed to accept login: " + err.Error())
o.Ctx.ResponseWriter.WriteHeader(500)
o.Data["json"] = map[string]string{"error": err.Error()}
o.ServeJSON()
return
}
// Return redirect_to so the frontend follows the OAuth2 flow
o.Data["json"] = redirect
o.ServeJSON()
}
// @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"
// @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.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 != "" {
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"
// @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()
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
}
o.Data["json"] = redirect
o.ServeJSON()
}
// @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()
} }
@@ -135,15 +378,15 @@ var whitelist = []string{
"/login", "/login",
"/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" // @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")
reqToken := o.Ctx.Request.Header.Get("Authorization") reqToken := o.Ctx.Request.Header.Get("Authorization")
if reqToken == "" { if reqToken == "" {
for _, w := range whitelist { for _, w := range whitelist {
@@ -164,7 +407,7 @@ func (o *OAuthController) InternaisDraftlAuthForward() {
reqToken = splitToken[1] reqToken = splitToken[1]
} }
origin, publicKey, external := o.extractOrigin(o.Ctx.Request) origin, publicKey, external := o.extractOrigin(o.Ctx.Request)
if !infrastructure.GetAuthConnector().CheckAuthForward( //reqToken != "" && if !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 != "" { o.Ctx.Request.Header.Get("X-Forwarded-Uri"), external) && origin != "" && publicKey != "" {
@@ -189,15 +432,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 +449,70 @@ 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 ""
} }
// 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,25 @@ services:
container_name: oc-auth container_name: oc-auth
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.auth.entrypoints=web"
- "traefik.http.routers.auth.rule=PathPrefix(`/auth`)"
- "traefik.http.middlewares.auth-rewrite.replacepathregex.regex=^/auth(.*)"
- "traefik.http.middlewares.auth-rewrite.replacepathregex.replacement=/oc$$1"
- "traefik.http.routers.auth.middlewares=auth-rewrite"
- "traefik.http.services.auth.loadbalancer.server.port=8080"
- "traefik.http.middlewares.auth.forwardauth.address=http://oc-auth:8080/oc/forward" - "traefik.http.middlewares.auth.forwardauth.address=http://oc-auth:8080/oc/forward"
- "traefik.http.routers.workflow.rule=PathPrefix(/auth)"
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="

95
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-20250211081618-d82ae166a1e5 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,7 +34,6 @@ 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/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
@@ -99,10 +41,7 @@ require (
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 +50,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 +62,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
) )

1085
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,119 @@
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) GetLoginChallenge(challenge string) (*LoginChallenge, error)
Refresh(client_id string, token *Token) (*Token, 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)
// Forward auth
CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) bool CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) bool
} }
// 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,7 +1,6 @@
package auth_connectors package auth_connectors
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@@ -10,10 +9,7 @@ import (
"net/url" "net/url"
"oc-auth/conf" "oc-auth/conf"
"oc-auth/infrastructure/claims" "oc-auth/infrastructure/claims"
"regexp"
"strconv"
"strings" "strings"
"time"
oclib "cloud.o-forge.io/core/oc-lib" oclib "cloud.o-forge.io/core/oc-lib"
"cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/models/peer"
@@ -21,17 +17,16 @@ import (
) )
type HydraConnector struct { type HydraConnector struct {
State string `json:"state"`
Scopes string `json:"scope"`
ResponseType string `json:"response_type"`
Caller *tools.HTTPCaller 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 +39,328 @@ 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 { // 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 and checks permissions from the token's extra claims
func (h *HydraConnector) CheckAuthForward(reqToken string, publicKey string, host string, method string, forward string, external bool) bool {
if forward == "" || method == "" { if forward == "" || method == "" {
return false return false
} }
var c claims.Claims logger := oclib.GetLogger()
token := strings.Split(reqToken, ".")
if len(token) > 2 { // Introspect the token via Hydra to get claims
bytes, err := base64.StdEncoding.DecodeString(token[2]) result, err := h.Introspect(reqToken)
if err != nil || !result.Active {
if err != nil { if err != nil {
return false logger.Error().Msg("Forward auth introspect failed: " + err.Error())
} }
err = json.Unmarshal(bytes, &c) return false
if err != nil { }
return false
// Extract claims from the introspection result's extra data
// Hydra puts consent session's access_token data in the "ext" field of introspection
var sessionClaims claims.Claims
if result.Extra != nil {
sessionClaims.Session.AccessToken = make(map[string]interface{})
sessionClaims.Session.IDToken = make(map[string]interface{})
for k, v := range result.Extra {
sessionClaims.Session.AccessToken[k] = v
} }
} }
// ask keto for permission is in claims
ok, err := claims.GetClaims().DecodeClaimsInToken(host, method, forward, c, publicKey, external) // Also try to get id_token claims from the token if it's a JWT
// For now, use the introspected extra claims and the peer signature verification
if sessionClaims.Session.IDToken == nil {
sessionClaims.Session.IDToken = make(map[string]interface{})
}
// Get self peer for signature verification
pp := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), "", "", []string{}, nil).Search(nil, fmt.Sprintf("%v", peer.SELF.EnumIndex()), false)
if len(pp.Data) > 0 {
p := pp.Data[0].(*peer.Peer)
// Re-sign for local verification if this is our own peer
if !external && p.PublicKey == publicKey {
sessionClaims.Session.IDToken["signature"] = ""
// For internal requests, skip signature check by using the claims decoder directly
ok, err := claims.GetClaims().DecodeClaimsInToken(host, method, forward, sessionClaims, publicKey, external)
if err != nil {
logger.Error().Msg("Failed to decode claims: " + err.Error())
}
return ok
}
}
ok, err := claims.GetClaims().DecodeClaimsInToken(host, method, forward, sessionClaims, publicKey, external)
if err != nil { if err != nil {
fmt.Println("Failed to decode claims", err) logger.Error().Msg("Failed to decode claims: " + err.Error())
} }
return ok return ok
} }
// 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

@@ -2,23 +2,28 @@ package claims
import ( import (
"oc-auth/conf" "oc-auth/conf"
"strings"
"cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/models/peer"
) )
// 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.
// Claims are injected into the Hydra JWT via the consent session, not appended to the token.
BuildConsentSession(clientID string, userId string, peer *peer.Peer) Claims
// DecodeClaimsInToken verifies permissions from claims extracted from a JWT
DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, error) DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, error)
} }
// SessionClaims 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"`
} }
@@ -28,5 +33,10 @@ var t = map[string]ClaimService{
} }
func GetClaims() ClaimService { func GetClaims() ClaimService {
return t[conf.GetConfig().Auth] 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,29 +80,33 @@ 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()
return now <= exp
}
func (h HydraClaims) DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, error) { func (h HydraClaims) DecodeClaimsInToken(host string, method string, forward string, sessionClaims Claims, publicKey string, external bool) (bool, error) {
logger := oclib.GetLogger()
idTokenClaims := sessionClaims.Session.IDToken idTokenClaims := sessionClaims.Session.IDToken
if idTokenClaims["signature"] == nil {
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 {
pStr, ok := p.(string)
if !ok {
continue
}
match := true match := true
splittedP := h.clearBlank(strings.Split(p.(string), "/")) splittedP := h.clearBlank(strings.Split(pStr, "/"))
if len(splittedP) != len(splittedPath) { if len(splittedP) != len(splittedPath) {
continue continue
} }
@@ -118,45 +125,53 @@ 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), nil
} }
} }
logger.Error().Msg("No permission found for " + method + " " + forward)
return false, errors.New("no permission found") 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.IDToken["username"] = userId
// we should get group from user c.Session.IDToken["peer_id"] = p.UUID
c.Session.IDToken["client_id"] = clientID
groups, err := perms_connectors.KetoConnector{}.GetGroupByUser(userId) groups, err := perms_connectors.KetoConnector{}.GetGroupByUser(userId)
if err != nil { if err != nil {
return claims logger.Error().Msg("Failed to get groups for user " + userId + ": " + err.Error())
return c
} }
claims.Session.IDToken["client_id"] = clientID c.Session.IDToken["groups"] = groups
claims.Session.IDToken["groups"] = groups c.Session.IDToken["signature"] = sign
claims.Session.IDToken["signature"] = sign return c
return claims
} }

View File

@@ -56,7 +56,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 +81,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
@@ -194,7 +197,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 +221,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 +257,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,19 +352,22 @@ 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{
@@ -382,12 +393,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

157
main.go
View File

@@ -2,19 +2,14 @@ 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" "time"
"strconv"
"strings"
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,23 +21,26 @@ 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")
conf.GetConfig().Local = o.GetBoolDefault("LOCAL", true)
// config LDAP // config LDAP
conf.GetConfig().SourceMode = o.GetStringDefault("SOURCE_MODE", "ldap") conf.GetConfig().SourceMode = o.GetStringDefault("SOURCE_MODE", "ldap")
@@ -50,96 +48,81 @@ func main() {
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 {
logger.Error().Msg("Failed to create admin role, retrying in 10s: " + err.Error())
time.Sleep(10 * time.Second)
continue
}
conn.BindRole(conf.GetConfig().AdminRole, "admin")
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.CreatePermission(k, p.(string), true)
}
} }
} }
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,50 @@
}, },
"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"
}
],
"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 +248,148 @@
"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",
"name": "body",
"description": "The workflow content",
"required": true,
"schema": {
"$ref": "#/definitions/models.workflow"
}
},
{ {
"in": "query", "in": "query",
"name": "client_id", "name": "login_challenge",
"description": "the client_id you want to get", "description": "The login challenge from Hydra",
"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.PostLogin",
"parameters": [
{
"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",
"name": "Authorization",
"description": "auth token",
"type": "string"
},
{ {
"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, "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 +568,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 +798,152 @@
} }
}, },
"definitions": { "definitions": {
"models.Token": { "2111.0xc0004ce750.false": {
"title": "Token", "title": "false",
"type": "object" "type": "object"
}, },
"models.workflow": { "3850.0xc0004ce930.false": {
"title": "workflow", "title": "false",
"type": "object"
},
"auth_connectors.IntrospectResult": {
"title": "IntrospectResult",
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"client_id": {
"type": "string"
},
"exp": {
"type": "integer",
"format": "int64"
},
"ext": {
"$ref": "#/definitions/3850.0xc0004ce930.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/2111.0xc0004ce750.false"
},
"request_url": {
"type": "string"
},
"session_id": {
"type": "string"
},
"skip": {
"type": "boolean"
},
"subject": {
"type": "string"
}
}
},
"auth_connectors.LoginRequest": {
"title": "LoginRequest",
"type": "object",
"properties": {
"login_challenge": {
"type": "string"
},
"password": {
"type": "string"
},
"username": {
"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,41 @@ 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
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 +187,110 @@ 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
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.PostLogin
parameters: parameters:
- 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 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 +422,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 +593,106 @@ paths:
"200": "200":
description: "" description: ""
definitions: definitions:
models.Token: 2111.0xc0004ce750.false:
title: "false"
type: object
3850.0xc0004ce930.false:
title: "false"
type: object
auth_connectors.IntrospectResult:
title: IntrospectResult
type: object
properties:
active:
type: boolean
client_id:
type: string
exp:
type: integer
format: int64
ext:
$ref: '#/definitions/3850.0xc0004ce930.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/2111.0xc0004ce750.false'
request_url:
type: string
session_id:
type: string
skip:
type: boolean
subject:
type: string
auth_connectors.LoginRequest:
title: LoginRequest
type: object
properties:
login_challenge:
type: string
password:
type: string
username:
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