Files
base/apis/record_auth_methods.go
zeekay 6ffd5c83f9 logger: switch attr-helpers to variadic positional pairs
Every non-handler call site that wrapped a value in
slog.String/Int/Bool/Any/Float64/Uint64 was passing an Attr through the
variadic any... slot of (*slog.Logger).Info/Warn/Error/Debug; the same
slot accepts the bare "key", value pair with identical semantics. Drop
the wrapper so the surface is one level less indirect and so the next
two commits can swap (*slog.Logger) for a small Logger interface
without rewriting every attr expression as well.

23 files, 156 call sites collapsed; no behavior change. Five files —
tools/logger/log.go, tools/logger/batch_handler.go +_test, and
core/log_printer.go +_test — are the handler-implementation layer and
legitimately reference slog.Attr/Level/Group; they were skipped.
2026-05-15 03:50:49 -07:00

183 lines
5.2 KiB
Go

package apis
import (
"net/http"
"os"
"strings"
"github.com/hanzoai/base/core"
"github.com/hanzoai/base/tools/auth"
"github.com/hanzoai/base/tools/security"
"golang.org/x/oauth2"
)
// externalIAMAuthMethods returns the auth-methods response a client
// sees when Base is configured with an external IAM. It exposes a
// single OAuth2 provider named "iam" — generic, no brand string in
// Base code. The IAM endpoint is recovered from the JWKS URL the
// platform plugin stored at boot.
//
// IAM_DISPLAY_NAME (optional env) feeds the UI button label; if
// unset, DisplayName is empty and the UI renders a neutral fallback.
func externalIAMAuthMethods(e *core.RequestEvent) authMethodsResponse {
jwksURL, _ := e.App.Store().Get(StoreKeyJWKSURL).(string)
base := strings.TrimSuffix(jwksURL, "/.well-known/jwks")
state := security.RandomString(30)
verifier := security.RandomString(43)
challenge := security.S256Challenge(verifier)
authURL := ""
if base != "" {
authURL = base + "/oauth/authorize?response_type=code&state=" + state +
"&code_challenge=" + challenge +
"&code_challenge_method=S256" +
"&redirect_uri="
}
info := providerInfo{
Name: "iam",
DisplayName: os.Getenv("IAM_DISPLAY_NAME"),
State: state,
AuthURL: authURL,
AuthUrl: authURL,
CodeVerifier: verifier,
CodeChallenge: challenge,
CodeChallengeMethod: "S256",
}
resp := authMethodsResponse{
OAuth2: oauth2Response{
Enabled: true,
Providers: []providerInfo{info},
},
}
resp.AuthProviders = resp.OAuth2.Providers
return resp
}
type oauth2Response struct {
Providers []providerInfo `json:"providers"`
Enabled bool `json:"enabled"`
}
type providerInfo struct {
Name string `json:"name"`
DisplayName string `json:"displayName"`
State string `json:"state"`
AuthURL string `json:"authURL"`
// @todo
// deprecated: use AuthURL instead
// AuthUrl will be removed after dropping v0.22 support
AuthUrl string `json:"authUrl"`
// technically could be omitted if the provider doesn't support PKCE,
// but to avoid breaking existing typed clients we'll return them as empty string
CodeVerifier string `json:"codeVerifier"`
CodeChallenge string `json:"codeChallenge"`
CodeChallengeMethod string `json:"codeChallengeMethod"`
}
type authMethodsResponse struct {
OAuth2 oauth2Response `json:"oauth2"`
// legacy alias kept so older SDK clients that read `authProviders`
// instead of `oauth2.providers` continue to work.
AuthProviders []providerInfo `json:"authProviders"`
}
func (amr *authMethodsResponse) fillLegacyFields() {
if amr.OAuth2.Enabled {
amr.AuthProviders = amr.OAuth2.Providers
}
}
func recordAuthMethods(e *core.RequestEvent) error {
collection, err := findAuthCollection(e)
if err != nil {
return err
}
// In IAM-native mode (the only mode the platform plugin allows)
// every collection — including _superusers — advertises a single
// generic "iam" OAuth2 provider. Base does not name the provider;
// IAM does. The local IAM_DISPLAY_NAME env (if set) becomes the
// UI label; otherwise the client renders a neutral "Sign in"
// button.
if externalOnly, _ := e.App.Store().Get(StoreKeyExternalAuthOnly).(bool); externalOnly {
return e.JSON(http.StatusOK, externalIAMAuthMethods(e))
}
// Standalone Base (no IAM configured) — keep the OAuth2 provider
// discovery response so the admin panel can still show whatever
// providers were registered on the collection.
result := authMethodsResponse{
OAuth2: oauth2Response{
Providers: make([]providerInfo, 0, len(collection.OAuth2.Providers)),
},
}
if !collection.OAuth2.Enabled {
result.fillLegacyFields()
return e.JSON(http.StatusOK, result)
}
result.OAuth2.Enabled = true
for _, config := range collection.OAuth2.Providers {
provider, err := config.InitProvider()
if err != nil {
e.App.Logger().Debug(
"Failed to setup OAuth2 provider",
"name", config.Name,
"error", err.Error(),
)
continue // skip provider
}
info := providerInfo{
Name: config.Name,
DisplayName: provider.DisplayName(),
State: security.RandomString(30),
}
if info.DisplayName == "" {
info.DisplayName = config.Name
}
urlOpts := []oauth2.AuthCodeOption{}
// custom providers url options
switch config.Name {
case auth.NameApple:
// see https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms#3332113
urlOpts = append(urlOpts, oauth2.SetAuthURLParam("response_mode", "form_post"))
}
if provider.PKCE() {
info.CodeVerifier = security.RandomString(43)
info.CodeChallenge = security.S256Challenge(info.CodeVerifier)
info.CodeChallengeMethod = "S256"
urlOpts = append(urlOpts,
oauth2.SetAuthURLParam("code_challenge", info.CodeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", info.CodeChallengeMethod),
)
}
info.AuthURL = provider.BuildAuthURL(
info.State,
urlOpts...,
) + "&redirect_uri=" // empty redirect_uri so that users can append their redirect url
info.AuthUrl = info.AuthURL
result.OAuth2.Providers = append(result.OAuth2.Providers, info)
}
result.fillLegacyFields()
return e.JSON(http.StatusOK, result)
}