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:
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>