128 lines
3.5 KiB
Go
128 lines
3.5 KiB
Go
package zitadel
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// verifyPasswordSession checks credentials via ZITADEL v2 Sessions API (PAT-backed).
|
|
// Used when OAuth resource-owner password grant is unavailable (default in ZITADEL v2).
|
|
func (c *Client) verifyPasswordSession(ctx context.Context, loginName, password string) (*TokenResult, error) {
|
|
if c.conf.ServiceUserToken == "" {
|
|
return nil, fmt.Errorf("zitadel: service user token is required for session password verification")
|
|
}
|
|
loginName = strings.TrimSpace(loginName)
|
|
if loginName == "" || password == "" {
|
|
return nil, ErrInvalidCredentials
|
|
}
|
|
|
|
var created struct {
|
|
SessionID string `json:"sessionId"`
|
|
}
|
|
if err := c.doSessionJSON(ctx, http.MethodPost, c.apiBase+"/v2/sessions", map[string]any{
|
|
"checks": map[string]any{
|
|
"user": map[string]any{"loginName": loginName},
|
|
},
|
|
}, &created); err != nil {
|
|
if isSessionUserNotFound(err) {
|
|
return nil, ErrInvalidCredentials
|
|
}
|
|
return nil, err
|
|
}
|
|
if created.SessionID == "" {
|
|
return nil, fmt.Errorf("zitadel: create session: empty session id")
|
|
}
|
|
|
|
if err := c.doSessionJSON(ctx, http.MethodPatch, c.apiBase+"/v2/sessions/"+created.SessionID, map[string]any{
|
|
"checks": map[string]any{
|
|
"password": map[string]any{"password": password},
|
|
},
|
|
}, nil); err != nil {
|
|
if isSessionPasswordInvalid(err) {
|
|
return nil, ErrInvalidCredentials
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var got struct {
|
|
Session struct {
|
|
Factors struct {
|
|
User struct {
|
|
ID string `json:"id"`
|
|
LoginName string `json:"loginName"`
|
|
} `json:"user"`
|
|
Password struct {
|
|
VerifiedAt string `json:"verifiedAt"`
|
|
} `json:"password"`
|
|
} `json:"factors"`
|
|
} `json:"session"`
|
|
}
|
|
if err := c.doSessionJSON(ctx, http.MethodGet, c.apiBase+"/v2/sessions/"+created.SessionID, nil, &got); err != nil {
|
|
return nil, err
|
|
}
|
|
if got.Session.Factors.Password.VerifiedAt == "" || got.Session.Factors.User.ID == "" {
|
|
return nil, ErrInvalidCredentials
|
|
}
|
|
return &TokenResult{
|
|
Subject: got.Session.Factors.User.ID,
|
|
Email: got.Session.Factors.User.LoginName,
|
|
}, nil
|
|
}
|
|
|
|
func (c *Client) doSessionJSON(ctx context.Context, method, endpoint string, body any, out any) error {
|
|
var r io.Reader
|
|
if body != nil {
|
|
raw, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("zitadel: marshal request: %w", err)
|
|
}
|
|
r = bytes.NewReader(raw)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, endpoint, r)
|
|
if err != nil {
|
|
return fmt.Errorf("zitadel: new request: %w", err)
|
|
}
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("Authorization", c.serviceAuth())
|
|
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("zitadel: %s %s: %w", method, endpoint, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
raw, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("zitadel: read response body: %w", err)
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return fmt.Errorf("zitadel: %s %s: status %d: %s", method, endpoint, resp.StatusCode, truncateBody(raw))
|
|
}
|
|
if out != nil && len(raw) > 0 {
|
|
if err := json.Unmarshal(raw, out); err != nil {
|
|
return fmt.Errorf("zitadel: decode response: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isSessionUserNotFound(err error) bool {
|
|
return err != nil && strings.Contains(err.Error(), "status 404")
|
|
}
|
|
|
|
func isSessionPasswordInvalid(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
s := err.Error()
|
|
return strings.Contains(s, "status 400") && strings.Contains(s, "Password is invalid")
|
|
}
|