commit 076986ae4f8c202ec22f3903d9bf12fbd8eda27f
parent eed82632a2fe194e2c33741a264d607c1b841708
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date: Thu, 18 Dec 2025 12:25:55 +0900
oidc: add most of the OIDC flow
Diffstat:
4 files changed, 121 insertions(+), 20 deletions(-)
diff --git a/pkg/taldir/command_validator.go b/pkg/taldir/command_validator.go
@@ -88,7 +88,7 @@ func (t *CommandValidator) RegistrationStart(topic string, link string, message
return "", nil
}
-func make_command_validator(cfg *TaldirConfig, name string, landingPageTpl *template.Template) CommandValidator {
+func makeCommandValidator(cfg *TaldirConfig, name string, landingPageTpl *template.Template) CommandValidator {
sec := cfg.Ini.Section("taldir-validator-" + name)
return CommandValidator{
name: name,
diff --git a/pkg/taldir/disseminator_gns.go b/pkg/taldir/disseminator_gns.go
@@ -84,7 +84,7 @@ func (d *GnsDisseminator) IsEnabled() bool {
return d.config.Ini.Section("taldir-disseminator-gns").Key("enabled").MustBool(false)
}
-func make_gns_disseminator(cfg *TaldirConfig) GnsDisseminator {
+func makeGnsDisseminator(cfg *TaldirConfig) GnsDisseminator {
d := GnsDisseminator{
config: cfg,
}
diff --git a/pkg/taldir/oidc_validator.go b/pkg/taldir/oidc_validator.go
@@ -19,9 +19,14 @@
package taldir
import (
+ "crypto/rand"
+ "encoding/json"
"fmt"
"html/template"
+ "net/http"
+ "net/url"
"regexp"
+ "strings"
)
type OidcValidator struct {
@@ -33,13 +38,19 @@ type OidcValidator struct {
config *TaldirConfig
// Client ID
- clientId string
+ clientID string
// Client secret
clientSecret string
- // Callback URI
- callbackUri string
+ // Scope(s)
+ scope string
+
+ // Redirect URI
+ redirectURI string
+
+ // Token endpoint
+ tokenEndpoint string
// OIDC authorization endpoint
authorizationEndpoint string
@@ -49,6 +60,21 @@ type OidcValidator struct {
// Validator alias regex
validAliasRegex string
+
+ // State object
+ // Maps states to challenge
+ authorizationsState map[string][]string
+}
+
+type OidcTokenResponse struct {
+ // AccessToken
+ AccessToken string `json:"access_token"`
+
+ // Token type
+ TokenType string `json:"token_type"`
+
+ // Expiration
+ ExpiresIn int `json:"expires_in"`
}
func (t *OidcValidator) LandingPageTpl() *template.Template {
@@ -81,21 +107,70 @@ func (t *OidcValidator) IsAliasValid(alias string) (err error) {
return
}
+func (t *OidcValidator) ProcessOidcCallback(r *http.Request) (string, string, error) {
+ // Process authorization code
+ state := r.URL.Query().Get("state")
+ if state == "" {
+ return "", "", fmt.Errorf("no state query parameter provided")
+ }
+ if t.authorizationsState[state] == nil {
+ return "", "", fmt.Errorf("state invalid")
+ }
+ // TODO process authorization code
+ code := r.URL.Query().Get("code")
+ data := url.Values{}
+ data.Set("client_id", t.clientID)
+ data.Set("grant_type", "authorization_code")
+ data.Set("redirect_uri", t.redirectURI)
+ data.Set("code", code)
+
+ req, err := http.NewRequest("POST", t.tokenEndpoint, strings.NewReader(data.Encode()))
+ if err != nil {
+ return "", "", fmt.Errorf("failed to create token request")
+ }
+ req.SetBasicAuth(t.clientID, t.clientSecret)
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", "", fmt.Errorf("failed to execute token request")
+ }
+ if resp.StatusCode != http.StatusOK {
+ return "", "", fmt.Errorf("unexpected response code %d", resp.StatusCode)
+ }
+ // TODO unmarshal JSON, retrieve/check against state, return hAlias and challenge
+ var tokenResponse OidcTokenResponse
+ err = json.NewDecoder(r.Body).Decode(&tokenResponse)
+ if err != nil {
+ return "", "", fmt.Errorf("unable to parse token response")
+ }
+ // TODO check against state, return hAlias and challenge
+ return "FIXME", "FIXME", nil
+}
+
func (t *OidcValidator) RegistrationStart(topic string, link string, message string, alias string, challenge string) (string, error) {
- // FIXME
- return t.authorizationEndpoint, nil
+ state := rand.Text()
+ t.authorizationsState[state] = []string{alias, challenge}
+ redirectURI := fmt.Sprintf("%s?response_type=code&redirect_uri=%s&client_id=%s&scope=%s&state=%s", t.authorizationEndpoint, t.redirectURI, t.clientID, t.scope, state)
+ return redirectURI, nil
}
-func make_oidc_validator(cfg *TaldirConfig, name string, landingPageTpl *template.Template) OidcValidator {
+func makeOidcValidator(cfg *TaldirConfig, name string, landingPageTpl *template.Template) OidcValidator {
+ mainSec := cfg.Ini.Section("taldir")
+ baseURL := mainSec.Key("base_url").MustString("")
+ // FIXME escape URI?
+ redirectURI := fmt.Sprintf("%s/%s", baseURL, name)
sec := cfg.Ini.Section("taldir-validator-" + name)
return OidcValidator{
name: name,
config: cfg,
landingPageTpl: landingPageTpl,
- clientId: sec.Key("client_id").MustString(""),
+ clientID: sec.Key("client_id").MustString(""),
clientSecret: sec.Key("client_secret").MustString(""),
- callbackUri: sec.Key("callback_uri").MustString(""),
+ scope: sec.Key("scope").MustString("openid"),
+ tokenEndpoint: sec.Key("token_endpoint").MustString(""),
authorizationEndpoint: sec.Key("authorization_endpoint").MustString(""),
validAliasRegex: sec.Key("valid_alias_regex").MustString(""),
+ redirectURI: redirectURI,
}
}
diff --git a/pkg/taldir/taldir.go b/pkg/taldir/taldir.go
@@ -16,6 +16,7 @@
//
// SPDX-License-Identifier: AGPL3.0-or-later
+// Package taldir implements the taler directory service.
package taldir
/* TODO
@@ -116,7 +117,7 @@ type Taldir struct {
MonthlyFee string
// Registrar base URL
- BaseUrl string
+ BaseURL string
// Currency Spec
CurrencySpec talerutil.CurrencySpecification
@@ -563,7 +564,7 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request) {
if len(validation.OrderID) == 0 {
// Add new order for new validations
// FIXME: What is the URL we want to provide here?
- orderID, newOrderErr := t.Merchant.AddNewOrder(*cost, "Taldir registration", t.BaseUrl)
+ orderID, newOrderErr := t.Merchant.AddNewOrder(*cost, "Taldir registration", t.BaseURL)
if newOrderErr != nil {
fmt.Println(newOrderErr)
w.WriteHeader(http.StatusInternalServerError)
@@ -579,7 +580,7 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request) {
if paytoErr != nil {
fmt.Println(paytoErr)
w.WriteHeader(http.StatusInternalServerError)
- t.Logger.Logf(LogError, paytoErr.Error()+"\n")
+ t.Logger.Logf(LogError, "%s\n", err.Error())
return
}
if len(payto) != 0 {
@@ -592,7 +593,7 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request) {
}
err = t.Db.Save(&validation).Error
if err != nil {
- t.Logger.Logf(LogError, err.Error()+"\n")
+ t.Logger.Logf(LogError, "%s\n", err.Error())
w.WriteHeader(500)
return
}
@@ -601,7 +602,7 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request) {
message := t.I18n.GetLocale(r).GetMessage("taldirRegMessage", link)
redirectionLink, err := validator.RegistrationStart(topic, link, message, req.Alias, validation.Challenge)
if err != nil {
- t.Logger.Logf(LogError, err.Error()+"\n")
+ t.Logger.Logf(LogError, "%s\n", err.Error())
t.Db.Delete(&validation)
w.WriteHeader(500)
return
@@ -616,6 +617,28 @@ func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(202)
}
+func (t *Taldir) oidcValidatorResponse(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ for name, validator := range t.Validators {
+ if validator.Type() != ValidatorTypeOIDC {
+ continue
+ }
+ if name != vars["validator"] {
+ continue
+ }
+ oidcValidator := validator.(*OidcValidator)
+ hAlias, challenge, err := oidcValidator.ProcessOidcCallback(r)
+ if err != nil {
+ t.Logger.Logf(LogError, "%s\n", err.Error())
+ w.WriteHeader(500)
+ return
+ }
+ http.Redirect(w, r, fmt.Sprintf("/register/%s/%s", hAlias, challenge), http.StatusSeeOther)
+ return
+ }
+ w.WriteHeader(http.StatusNotFound)
+}
+
func (t *Taldir) configResponse(w http.ResponseWriter, r *http.Request) {
meths := []AliasType{}
i := 0
@@ -827,13 +850,13 @@ func (t *Taldir) typeLookupResultPage(w http.ResponseWriter, r *http.Request) {
}
}
encodedPng := ""
- talerAddContactURI := "";
+ talerAddContactURI := ""
if found && strings.HasPrefix(entry.TargetURI, "https://") {
// This could be a mailbox URI and we can create a helper QR code for import
hostDomain := strings.TrimPrefix(entry.TargetURI, "https://")
talerAddContactURI, err = url.JoinPath("taler://add-contact", val.Name(), r.URL.Query().Get("alias"), hostDomain)
if nil == err {
- talerAddContactURI += "?sourceBaseUrl=" + url.QueryEscape(t.BaseUrl)
+ talerAddContactURI += "?sourceBaseUrl=" + url.QueryEscape(t.BaseURL)
qrPng, qrErr := qrcode.Encode(talerAddContactURI, qrcode.Medium, 256)
if qrErr != nil {
t.Logger.Logf(LogError, "Failed to create QR code")
@@ -912,6 +935,9 @@ func (t *Taldir) setupHandlers() {
t.Router.HandleFunc("/register/{h_alias}/{challenge}", t.validationPage).Methods("GET")
t.Router.HandleFunc("/{h_alias}", t.validationRequest).Methods("POST")
+ // OIDC validator callback URI(s)
+ t.Router.HandleFunc("/oidc_validator/{validator}", t.oidcValidatorResponse).Methods("GET")
+
}
var pluralizeClient = pluralize.NewClient()
@@ -958,7 +984,7 @@ func (t *Taldir) Initialize(cfg TaldirConfig) {
navTplFile := cfg.Ini.Section("taldir").Key("navigation").MustString(t.getFileName("web/templates/nav.html"))
footerTplFile := cfg.Ini.Section("taldir").Key("footer").MustString(t.getFileName("web/templates/footer.html"))
- t.BaseUrl = cfg.Ini.Section("taldir").Key("base_url").MustString("http://localhost:11000")
+ t.BaseURL = cfg.Ini.Section("taldir").Key("base_url").MustString("http://localhost:11000")
t.Validators = make(map[string]Validator)
for _, sec := range cfg.Ini.Sections() {
if !strings.HasPrefix(sec.Name(), "taldir-validator-") {
@@ -979,7 +1005,7 @@ func (t *Taldir) Initialize(cfg TaldirConfig) {
t.Logger.Logf(LogWarning, "`%s` template not found, disabling validator `%s`.\n", vlandingPageTplFile, vname)
continue
}
- v := make_command_validator(&cfg, vname, vlandingPageTpl)
+ v := makeCommandValidator(&cfg, vname, vlandingPageTpl)
if v.IsEnabled() {
t.Logger.Logf(LogInfo, "`%s` validator disabled.\n", vname)
t.Validators[vname] = &v
@@ -988,7 +1014,7 @@ func (t *Taldir) Initialize(cfg TaldirConfig) {
}
t.Logger.Logf(LogDebug, "Found %d validators.\n", len(t.Validators))
t.Disseminators = make(map[string]Disseminator)
- gnsdisseminator := make_gns_disseminator(&cfg)
+ gnsdisseminator := makeGnsDisseminator(&cfg)
if gnsdisseminator.IsEnabled() {
t.Disseminators[gnsdisseminator.Name()] = &gnsdisseminator
t.Logger.Logf(LogInfo, "Disseminator `%s' enabled.\n", gnsdisseminator.Name())