Files
ragflow/internal/utility/oauth/oidc.go
web-dev0521 1696d4ead6 feat(go-api): implement password-reset flow (issue #15282) (#15293)
## Summary

Ports the Python password-reset flow to Go, adding 4 unauthenticated
endpoints under `/api/v1/auth/password/`:

- `POST /auth/password/forgot/captcha` — generates and returns a PNG
captcha image; stores the plaintext code in Redis (60 s TTL)
- `POST /auth/password/forgot/otp` — verifies captcha, enforces resend
cooldown (60 s), generates HMAC-SHA256-hashed OTP (300 s TTL), sends
plain-text email via SMTP
- `POST /auth/password/forgot/otp/verify` — verifies OTP with attempt
counting (lock after 5 failures for 30 min), sets a
`otp:verified:{email}` flag (300 s TTL) on success
- `POST /auth/password/reset` — checks verified flag, decrypts +
validates passwords, updates user record, auto-logs in (issues JWT,
returns user profile)

Closes #15282
2026-06-02 09:38:02 +08:00

108 lines
3.3 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package oauth
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
// oidcClient is the OIDC flavor: it resolves the authorization /
// token / userinfo URLs from the Issuer's discovery document
// (.well-known/openid-configuration) before delegating the OAuth flow to
// oauthClient.
//
// We do not currently verify or parse the id_token: id_token claims are
// only used as enrichment in the Python implementation, and the /userinfo
// endpoint (already called) returns the same canonical claims authenticated
// via the access_token. Tracked as a follow-up to add full JWKS-based
// id_token verification once a JWT library is vendored.
type oidcClient struct {
*oauthClient
issuer string
}
func newOIDCClient(cfg Config) (*oidcClient, error) {
if cfg.Issuer == "" {
return nil, fmt.Errorf("Missing issuer in configuration.")
}
meta, err := loadOIDCMetadata(cfg.Issuer)
if err != nil {
return nil, fmt.Errorf("Failed to fetch OIDC metadata: %w", err)
}
if meta.Issuer != "" {
cfg.Issuer = meta.Issuer
}
if cfg.AuthorizationURL == "" {
cfg.AuthorizationURL = meta.AuthorizationEndpoint
}
if cfg.TokenURL == "" {
cfg.TokenURL = meta.TokenEndpoint
}
if cfg.UserinfoURL == "" {
cfg.UserinfoURL = meta.UserinfoEndpoint
}
base, err := newOAuthClient(cfg)
if err != nil {
return nil, err
}
return &oidcClient{oauthClient: base, issuer: cfg.Issuer}, nil
}
// oidcMetadata is the subset of fields we use from the discovery document.
type oidcMetadata struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
JWKSURI string `json:"jwks_uri"`
}
// loadOIDCMetadata is the indirection used to fetch a provider's discovery
// document. Tests override it.
var loadOIDCMetadata = func(issuer string) (*oidcMetadata, error) {
metadataURL := strings.TrimRight(issuer, "/") + "/.well-known/openid-configuration"
ctx, cancel := context.WithTimeout(context.Background(), HTTPRequestTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, metadataURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
meta := &oidcMetadata{}
if err := json.Unmarshal(body, meta); err != nil {
return nil, fmt.Errorf("parse OIDC metadata: %w", err)
}
return meta, nil
}