taldir

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

commit 88548abf29f8fe7f5e633874026fbf6cd839628e
parent 184bb2a5fae4546e74914775dc6ea0f9a4d187f9
Author: Martin Schanzenbach <schanzen@gnunet.org>
Date:   Tue, 23 Dec 2025 14:36:32 +0900

towards oauth2 and mastodon support

Diffstat:
Mpkg/taldir/oidc_validator.go | 95+++++++++++++++++++++++++++----------------------------------------------------
Aweb/templates/landing_mastodon.html | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 92 insertions(+), 63 deletions(-)

diff --git a/pkg/taldir/oidc_validator.go b/pkg/taldir/oidc_validator.go @@ -27,9 +27,6 @@ import ( "net/url" "regexp" "strings" - - "github.com/go-jose/go-jose/v4" - "github.com/go-jose/go-jose/v4/jwt" ) type AuthorizationsState struct { @@ -41,6 +38,11 @@ type AuthorizationsState struct { challenge string } +type RelevantUserClaims struct { + // Subject + Sub string +} + type OidcValidator struct { // Name @@ -64,15 +66,12 @@ type OidcValidator struct { // Redirect URI redirectURI string + // Userinfo endpoint + userinfoEndpoint string + // Token endpoint tokenEndpoint string - // JWKS endpoint - jwksEndpoint string - - // Supported JWK algos - jwtAlgos []jose.SignatureAlgorithm - // OIDC authorization endpoint authorizationEndpoint string @@ -87,13 +86,10 @@ type OidcValidator struct { authorizationsState map[string]*AuthorizationsState } -type OidcTokenResponse struct { +type OAuthTokenResponse struct { // AccessToken AccessToken string `json:"access_token"` - // IDToken (OIDC only) - IDToken string `json:"id_token"` - // Token type TokenType string `json:"token_type"` @@ -127,52 +123,31 @@ func (t OidcValidator) IsAliasValid(alias string) (err error) { return } -func (t OidcValidator) ValidateIDToken(tokenString string, expectedAlias string) error { - var jwkSet jose.JSONWebKeySet - var claims jwt.Claims - token, err := jwt.ParseSigned(tokenString, t.jwtAlgos) + + +func (t OidcValidator) ValidateAliasSubject(tokenString string, expectedAlias string) error { + var relevantClaims RelevantUserClaims + req, err := http.NewRequest("GET", t.userinfoEndpoint, nil) if err != nil { - return fmt.Errorf("unable to parse token: %v", err) + return fmt.Errorf("failed to create userinfo request") } - if len(token.Headers) != 1 { - return fmt.Errorf("token has more than one header") + req.Header.Set("Authorization", "Bearer " + tokenString) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to execute userinfo request") } - if token.Headers[0].Algorithm == string(jose.HS256) || - token.Headers[0].Algorithm == string(jose.HS384) || - token.Headers[0].Algorithm == string(jose.HS512) { - // Verify and extract claims using the JWKS - err = token.Claims(t.sharedTokenSecret, &claims) - if err != nil { - return fmt.Errorf("unable to validate token claims: %v", err) - } - } else { - req, err := http.NewRequest("GET", t.jwksEndpoint, nil) - if err != nil { - return fmt.Errorf("failed to create JWKS request") - } - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return fmt.Errorf("failed to execute JWKS request") - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("unexpected response code %d", resp.StatusCode) - } - err = json.NewDecoder(resp.Body).Decode(&jwkSet) - if err != nil { - return fmt.Errorf("unable to parse JWKS response") - } - // Verify and extract claims using the JWKS - err = token.Claims(jwkSet, &claims) - if err != nil { - return fmt.Errorf("unable to validate token claims: %v", err) - } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected response code %d", resp.StatusCode) + } + err = json.NewDecoder(resp.Body).Decode(&relevantClaims) + if err != nil { + return fmt.Errorf("unable to parse userinfo response") } - if claims.Subject != expectedAlias { - return fmt.Errorf("subject in ID token (%s) does not match state (%s)", claims.Subject, expectedAlias) + if relevantClaims.Sub != expectedAlias { + return fmt.Errorf("subject in ID token (%s) does not match state (%s)", relevantClaims.Sub, expectedAlias) } return nil - } func (t OidcValidator) ProcessOidcCallback(r *http.Request) (string, string, error) { @@ -206,12 +181,12 @@ func (t OidcValidator) ProcessOidcCallback(r *http.Request) (string, string, err if resp.StatusCode != http.StatusOK { return "", "", fmt.Errorf("unexpected response code %d", resp.StatusCode) } - var tokenResponse OidcTokenResponse + var tokenResponse OAuthTokenResponse err = json.NewDecoder(resp.Body).Decode(&tokenResponse) if err != nil { return "", "", fmt.Errorf("unable to parse token response: %v", err) } - err = t.ValidateIDToken(tokenResponse.IDToken, t.authorizationsState[state].alias) + err = t.ValidateAliasSubject(tokenResponse.AccessToken, t.authorizationsState[state].alias) if err != nil { return "", "", fmt.Errorf("unable to validate token: %v", err) } @@ -231,25 +206,19 @@ func makeOidcValidator(cfg *TaldirConfig, name string, landingPageTpl *template. // FIXME escape URI? redirectURI := fmt.Sprintf("%s/oidc_validator/%s", baseURL, name) sec := cfg.Ini.Section("taldir-validator-" + name) - algos := strings.Split(sec.Key("jwt_algos").MustString("RS256"), ",") - algoCast := make([]jose.SignatureAlgorithm, 0) - for _, a := range algos { - algoCast = append(algoCast, jose.SignatureAlgorithm(a)) - } return OidcValidator{ name: name, config: cfg, landingPageTpl: landingPageTpl, clientID: sec.Key("client_id").MustString(""), clientSecret: sec.Key("client_secret").MustString(""), - scope: sec.Key("scope").MustString("openid"), + scope: sec.Key("scope").MustString("profile"), tokenEndpoint: sec.Key("token_endpoint").MustString(""), - jwksEndpoint: sec.Key("jwks_endpoint").MustString(""), sharedTokenSecret: sec.Key("shared_token_secret").MustString("secret"), + userinfoEndpoint: sec.Key("userinfo_endpoint").MustString(""), authorizationEndpoint: sec.Key("authorization_endpoint").MustString(""), validAliasRegex: sec.Key("valid_alias_regex").MustString(""), redirectURI: redirectURI, authorizationsState: make(map[string]*AuthorizationsState, 0), - jwtAlgos: algoCast, } } diff --git a/web/templates/landing_mastodon.html b/web/templates/landing_mastodon.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <!-- Required meta tags --> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <link href="/css/bootstrap.min.css" rel="stylesheet"> + <link href="/css/style.css" rel="stylesheet"> + <link href="/fontawesome/css/fontawesome.css" rel="stylesheet" /> + <link href="/fontawesome/css/solid.css" rel="stylesheet" /> + <title>{{ call .tr "title" }}</title> + </head> + <body> + {{ template "nav.html" . }} + <div class="container pt-5"> + {{if .error}} + <div class="container pt-5"> + <div id="ebanner" class="alert alert-danger" role="alert"> + <h4 class="alert-heading">{{ call .tr "error" }}</h4> + <hr> + <p class="mb-0">{{.error}}</p> + </div> + </div> + {{end}} + <div class="row"> + <div class="col-2"> + <ul class="timeline"> + <li class="inprogress">{{ call .tr "lookup" }}</li> + <li>{{ call .tr "registerOrModify" }}</li> + <li>{{ call .tr "confirm" }}</li> + </ul> + </div> + <div class="col-8"> + <div class="card"> + <div class="card-body"> + <h4 class="card-title">{{ call .tr "lookupEmail" }}</h4> + <hr> + <p class="card-text">{{ call .tr "lookupEmailDescription" }}</p> + </div> + <form method="get" action="/lookup/mastodon"> + <div class="row"> + <div class="col-lg-6 offset-lg-3 text-center"> + <div class="input-group mb-3"> + <div class="form-floating"> + <input type="text" name="alias" id="floatingInput" class="form-control" placeholder="@jdoe" aria-label="Default" aria-describedby="inputGroup-sizing-default" data-sb-validations="required,email"> + <label for="floatingInput">{{ call .tr "email" }}</label> + </div> + <div class="invalid-feedback text-white" data-sb-feedback="emailAddressBelow:required">Email Address is required.</div> + <div class="invalid-feedback text-white" data-sb-feedback="emailAddressBelow:email">Email Address is not valid.</div> + <input class="input-group-text btn btn-outline-primary" type="submit" value="{{ call .tr "lookup" }}"> + </div> + </div> + </div> + </form> + </div> + </div> + </div> + {{ template "footer.html" . }} + </body> +</html>