taldir

Directory service to resolve wallet mailboxes by messenger addresses
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpkg/taldir/command_validator.go | 2+-
Mpkg/taldir/disseminator_gns.go | 2+-
Mpkg/taldir/oidc_validator.go | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mpkg/taldir/taldir.go | 46++++++++++++++++++++++++++++++++++++----------
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())