2024-10-28 14:58:11 +01:00
|
|
|
package controllers
|
|
|
|
|
|
|
|
|
|
import (
|
2025-01-17 17:24:08 +01:00
|
|
|
"encoding/base64"
|
2024-10-28 14:58:11 +01:00
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
2025-01-17 17:24:08 +01:00
|
|
|
"oc-auth/conf"
|
2024-10-28 14:58:11 +01:00
|
|
|
"oc-auth/infrastructure"
|
|
|
|
|
auth_connectors "oc-auth/infrastructure/auth_connector"
|
2024-10-30 12:38:25 +01:00
|
|
|
"regexp"
|
2026-02-19 14:56:15 +01:00
|
|
|
"strconv"
|
2024-10-28 14:58:11 +01:00
|
|
|
"strings"
|
2025-04-01 10:16:26 +02:00
|
|
|
"time"
|
2024-10-28 14:58:11 +01:00
|
|
|
|
|
|
|
|
oclib "cloud.o-forge.io/core/oc-lib"
|
2026-01-23 09:40:38 +01:00
|
|
|
"cloud.o-forge.io/core/oc-lib/models/peer"
|
2024-10-28 14:58:11 +01:00
|
|
|
model "cloud.o-forge.io/core/oc-lib/models/peer"
|
|
|
|
|
beego "github.com/beego/beego/v2/server/web"
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-19 14:56:15 +01:00
|
|
|
// OAuthController handles OAuth2 login/consent provider endpoints
|
2024-10-28 14:58:11 +01:00
|
|
|
type OAuthController struct {
|
|
|
|
|
beego.Controller
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 14:56:15 +01:00
|
|
|
// @Title GetLogin
|
|
|
|
|
// @Description Hydra redirects here with a login_challenge. Returns challenge info or auto-accepts if session exists.
|
|
|
|
|
// @Param login_challenge query string true "The login challenge from Hydra"
|
|
|
|
|
// @Success 200 {object} auth_connectors.LoginChallenge
|
|
|
|
|
// @Failure 400 missing login_challenge
|
|
|
|
|
// @Failure 500 internal error
|
|
|
|
|
// @router /login [get]
|
|
|
|
|
func (o *OAuthController) GetLogin() {
|
|
|
|
|
logger := oclib.GetLogger()
|
|
|
|
|
challenge := o.Ctx.Input.Query("login_challenge")
|
|
|
|
|
if challenge == "" {
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(400)
|
|
|
|
|
o.Data["json"] = map[string]string{"error": "missing login_challenge parameter"}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
2024-10-28 14:58:11 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-19 14:56:15 +01:00
|
|
|
if conf.GetConfig().Local {
|
|
|
|
|
// In local mode, return a mock challenge for dev
|
|
|
|
|
o.Data["json"] = &auth_connectors.LoginChallenge{
|
|
|
|
|
Skip: false,
|
|
|
|
|
Challenge: challenge,
|
2025-04-01 10:16:26 +02:00
|
|
|
}
|
2026-02-19 14:56:15 +01:00
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loginChallenge, err := infrastructure.GetAuthConnector().GetLoginChallenge(challenge)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error().Msg("Failed to get login challenge: " + err.Error())
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(500)
|
|
|
|
|
o.Data["json"] = map[string]string{"error": err.Error()}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If skip is true, the user already has an active session — auto-accept
|
|
|
|
|
if loginChallenge.Skip {
|
|
|
|
|
redirect, err := infrastructure.GetAuthConnector().AcceptLogin(challenge, loginChallenge.Subject)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Error().Msg("Failed to auto-accept login: " + err.Error())
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(500)
|
|
|
|
|
o.Data["json"] = map[string]string{"error": err.Error()}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
o.Data["json"] = redirect
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
2024-10-28 14:58:11 +01:00
|
|
|
}
|
2026-02-19 14:56:15 +01:00
|
|
|
|
|
|
|
|
// Return challenge info so frontend can render login form
|
|
|
|
|
o.Data["json"] = loginChallenge
|
2024-10-28 14:58:11 +01:00
|
|
|
o.ServeJSON()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 14:56:15 +01:00
|
|
|
// @Title PostLogin
|
|
|
|
|
// @Description Authenticate user via LDAP and accept Hydra login challenge
|
|
|
|
|
// @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
|
2025-01-17 17:24:08 +01:00
|
|
|
// @router /login [post]
|
|
|
|
|
func (o *OAuthController) Login() {
|
2026-02-19 14:56:15 +01:00
|
|
|
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
|
2024-10-28 14:58:11 +01:00
|
|
|
}
|
2026-02-19 14:56:15 +01:00
|
|
|
|
|
|
|
|
if req.Username == "" || req.Password == "" {
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(400)
|
|
|
|
|
o.Data["json"] = map[string]string{"error": "username and password are required"}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Authenticate via LDAP
|
|
|
|
|
ldap := auth_connectors.New()
|
|
|
|
|
found, err := ldap.Authenticate(o.Ctx.Request.Context(), req.Username, req.Password)
|
|
|
|
|
if err != nil || !found {
|
|
|
|
|
logger.Error().Msg("LDAP authentication failed for user: " + req.Username)
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(401)
|
|
|
|
|
o.Data["json"] = map[string]string{"error": "invalid credentials"}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if conf.GetConfig().Local {
|
|
|
|
|
// In local mode, return a mock token for dev
|
2025-04-01 10:16:26 +02:00
|
|
|
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 {
|
2026-02-19 14:56:15 +01:00
|
|
|
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)
|
2025-04-01 10:16:26 +02:00
|
|
|
token := &auth_connectors.Token{
|
|
|
|
|
Active: true,
|
2026-02-19 14:56:15 +01:00
|
|
|
TokenType: "Bearer",
|
2025-04-01 10:16:26 +02:00
|
|
|
ExpiresIn: 3600,
|
2026-02-19 14:56:15 +01:00
|
|
|
AccessToken: "localtoken." + base64.StdEncoding.EncodeToString(b),
|
2025-04-01 10:16:26 +02:00
|
|
|
}
|
|
|
|
|
o.Data["json"] = token
|
|
|
|
|
} else {
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(401)
|
2026-02-19 14:56:15 +01:00
|
|
|
o.Data["json"] = map[string]string{"error": "peer not found"}
|
2025-04-01 10:16:26 +02:00
|
|
|
}
|
2026-02-19 14:56:15 +01:00
|
|
|
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
|
2024-10-28 14:58:11 +01:00
|
|
|
}
|
2026-02-19 14:56:15 +01:00
|
|
|
|
|
|
|
|
// Return redirect_to so the frontend follows the OAuth2 flow
|
|
|
|
|
o.Data["json"] = redirect
|
2024-10-28 14:58:11 +01:00
|
|
|
o.ServeJSON()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 14:56:15 +01:00
|
|
|
// @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() {
|
2025-01-17 17:24:08 +01:00
|
|
|
clientID := o.Ctx.Input.Query("client_id")
|
2026-02-19 14:56:15 +01:00
|
|
|
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,
|
2025-04-01 10:16:26 +02:00
|
|
|
}
|
2026-02-19 14:56:15 +01:00
|
|
|
}
|
|
|
|
|
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()}
|
2024-10-28 14:58:11 +01:00
|
|
|
} else {
|
2025-04-01 10:16:26 +02:00
|
|
|
o.Data["json"] = token
|
2024-10-28 14:58:11 +01:00
|
|
|
}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 14:56:15 +01:00
|
|
|
// @Title Introspect
|
|
|
|
|
// @Description Introspect a token — respects Hydra's response
|
|
|
|
|
// @Param Authorization header string false "Bearer token"
|
|
|
|
|
// @Success 200 {object} auth_connectors.IntrospectResult
|
2024-10-28 14:58:11 +01:00
|
|
|
// @router /introspect [get]
|
|
|
|
|
func (o *OAuthController) Introspect() {
|
2026-02-19 14:56:15 +01:00
|
|
|
reqToken := extractBearerToken(o.Ctx.Request)
|
|
|
|
|
if reqToken == "" {
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(401)
|
|
|
|
|
o.Data["json"] = map[string]string{"error": "missing bearer token"}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
2024-10-28 14:58:11 +01:00
|
|
|
}
|
2026-02-19 14:56:15 +01:00
|
|
|
|
|
|
|
|
if conf.GetConfig().Local {
|
|
|
|
|
o.Data["json"] = &auth_connectors.IntrospectResult{Active: true}
|
|
|
|
|
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.Data["json"] = result
|
|
|
|
|
} else {
|
|
|
|
|
o.Data["json"] = result
|
2024-10-28 14:58:11 +01:00
|
|
|
}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-30 12:38:25 +01:00
|
|
|
var whitelist = []string{
|
|
|
|
|
"/login",
|
|
|
|
|
"/refresh",
|
|
|
|
|
"/introspect",
|
2026-02-19 14:56:15 +01:00
|
|
|
"/consent",
|
2024-10-30 12:38:25 +01:00
|
|
|
}
|
|
|
|
|
|
2024-10-28 14:58:11 +01:00
|
|
|
// @Title AuthForward
|
2026-02-19 14:56:15 +01:00
|
|
|
// @Description Forward auth for Traefik — validates JWT via Hydra introspection
|
|
|
|
|
// @Param Authorization header string false "Bearer token"
|
2024-10-28 14:58:11 +01:00
|
|
|
// @Success 200 {string}
|
|
|
|
|
// @router /forward [get]
|
2025-04-01 10:16:26 +02:00
|
|
|
func (o *OAuthController) InternalAuthForward() {
|
2024-10-28 14:58:11 +01:00
|
|
|
reqToken := o.Ctx.Request.Header.Get("Authorization")
|
2024-10-30 12:38:25 +01:00
|
|
|
if reqToken == "" {
|
|
|
|
|
for _, w := range whitelist {
|
|
|
|
|
if strings.Contains(o.Ctx.Request.Header.Get("X-Forwarded-Uri"), w) {
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(200)
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
o.Ctx.ResponseWriter.WriteHeader(401)
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
2024-10-28 14:58:11 +01:00
|
|
|
splitToken := strings.Split(reqToken, "Bearer ")
|
|
|
|
|
if len(splitToken) < 2 {
|
|
|
|
|
reqToken = ""
|
|
|
|
|
} else {
|
|
|
|
|
reqToken = splitToken[1]
|
|
|
|
|
}
|
2025-01-17 17:24:08 +01:00
|
|
|
origin, publicKey, external := o.extractOrigin(o.Ctx.Request)
|
2026-02-19 14:56:15 +01:00
|
|
|
if !infrastructure.GetAuthConnector().CheckAuthForward(
|
2024-10-30 12:38:25 +01:00
|
|
|
reqToken, publicKey, origin,
|
|
|
|
|
o.Ctx.Request.Header.Get("X-Forwarded-Method"),
|
2024-10-30 16:39:52 +01:00
|
|
|
o.Ctx.Request.Header.Get("X-Forwarded-Uri"), external) && origin != "" && publicKey != "" {
|
2024-10-28 14:58:11 +01:00
|
|
|
o.Ctx.ResponseWriter.WriteHeader(401)
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
o.ServeJSON()
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-17 17:24:08 +01:00
|
|
|
func (o *OAuthController) extractOrigin(request *http.Request) (string, string, bool) {
|
|
|
|
|
user, peerID, groups := oclib.ExtractTokenInfo(*request)
|
2024-10-28 14:58:11 +01:00
|
|
|
external := true
|
|
|
|
|
publicKey := ""
|
|
|
|
|
origin := o.Ctx.Request.Header.Get("X-Forwarded-Host")
|
|
|
|
|
if origin == "" {
|
|
|
|
|
origin = o.Ctx.Request.Header.Get("Origin")
|
|
|
|
|
}
|
2024-10-30 12:38:25 +01:00
|
|
|
searchStr := origin
|
|
|
|
|
r := regexp.MustCompile("(:[0-9]+)")
|
|
|
|
|
t := r.FindString(searchStr)
|
|
|
|
|
if t != "" {
|
|
|
|
|
searchStr = strings.Replace(searchStr, t, "", -1)
|
|
|
|
|
}
|
2026-01-23 09:40:38 +01:00
|
|
|
pp := oclib.NewRequest(oclib.LibDataEnum(oclib.PEER), user, peerID, groups, nil).Search(nil, searchStr, false)
|
2026-02-19 14:56:15 +01:00
|
|
|
if pp.Code != 200 || len(pp.Data) == 0 {
|
2024-10-30 12:38:25 +01:00
|
|
|
return "", "", external
|
|
|
|
|
}
|
2026-01-23 09:40:38 +01:00
|
|
|
p := pp.Data[0].(*model.Peer)
|
2024-10-30 12:38:25 +01:00
|
|
|
publicKey = p.PublicKey
|
2026-02-02 10:00:44 +01:00
|
|
|
origin = p.APIUrl
|
2026-02-19 14:56:15 +01:00
|
|
|
if origin != "" {
|
2026-01-23 09:40:38 +01:00
|
|
|
if p.Relation == peer.SELF {
|
2024-10-28 14:58:11 +01:00
|
|
|
external = false
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
external = false
|
|
|
|
|
}
|
|
|
|
|
return origin, publicKey, external
|
|
|
|
|
}
|
2025-01-17 17:24:08 +01:00
|
|
|
|
2026-02-19 14:56:15 +01:00
|
|
|
// ExtractClient extracts the client_id from a JWT token.
|
|
|
|
|
// Supports both standard JWT (3 parts with base64 payload) and local dev tokens.
|
2025-01-17 17:24:08 +01:00
|
|
|
func ExtractClient(request http.Request) string {
|
|
|
|
|
reqToken := request.Header.Get("Authorization")
|
|
|
|
|
splitToken := strings.Split(reqToken, "Bearer ")
|
|
|
|
|
if len(splitToken) < 2 {
|
2026-02-19 14:56:15 +01:00
|
|
|
return ""
|
2025-01-17 17:24:08 +01:00
|
|
|
}
|
2026-02-19 14:56:15 +01:00
|
|
|
reqToken = splitToken[1]
|
|
|
|
|
if reqToken == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to decode as standard JWT (header.payload.signature)
|
|
|
|
|
parts := strings.Split(reqToken, ".")
|
|
|
|
|
if len(parts) >= 2 {
|
|
|
|
|
// Decode the payload (second part of JWT)
|
|
|
|
|
payload := parts[1]
|
|
|
|
|
// Add padding if needed
|
|
|
|
|
switch len(payload) % 4 {
|
|
|
|
|
case 2:
|
|
|
|
|
payload += "=="
|
|
|
|
|
case 3:
|
|
|
|
|
payload += "="
|
|
|
|
|
}
|
|
|
|
|
bytes, err := base64.URLEncoding.DecodeString(payload)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Try standard base64 for local dev tokens
|
|
|
|
|
bytes, err = base64.StdEncoding.DecodeString(parts[len(parts)-1])
|
2025-01-17 17:24:08 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
2026-02-19 14:56:15 +01:00
|
|
|
}
|
|
|
|
|
m := map[string]interface{}{}
|
|
|
|
|
if err := json.Unmarshal(bytes, &m); err != nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
// Standard JWT: look for client_id in top-level or ext claims
|
|
|
|
|
if cid, ok := m["client_id"].(string); ok {
|
|
|
|
|
return cid
|
|
|
|
|
}
|
|
|
|
|
if ext, ok := m["ext"].(map[string]interface{}); ok {
|
|
|
|
|
if cid, ok := ext["client_id"].(string); ok {
|
|
|
|
|
return cid
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Local dev token format: session.id_token.client_id
|
|
|
|
|
if session, ok := m["session"].(map[string]interface{}); ok {
|
|
|
|
|
if idToken, ok := session["id_token"].(map[string]interface{}); ok {
|
|
|
|
|
if cid, ok := idToken["client_id"].(string); ok {
|
|
|
|
|
return cid
|
|
|
|
|
}
|
2025-01-17 17:24:08 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
2026-02-19 14:56:15 +01:00
|
|
|
|
|
|
|
|
// 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]
|
|
|
|
|
}
|