69 lines
1.7 KiB
Go
69 lines
1.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
)
|
|
|
|
// PhoneClient fetches a per-request FIDO2 assertion token from Component 2.
|
|
// There is no session caching — each call triggers a card interaction.
|
|
type PhoneClient struct {
|
|
baseURL string
|
|
}
|
|
|
|
func NewPhoneClient(baseURL string) *PhoneClient {
|
|
return &PhoneClient{baseURL: baseURL}
|
|
}
|
|
|
|
// GetTokenForRequest calls /auth/get-token with url+method+nonce, triggering
|
|
// a fresh FIDO2 assertion on the card. Returns the self-contained assertion
|
|
// bundle that the endpoint can verify independently.
|
|
func (c *PhoneClient) GetTokenForRequest(rawURL, method string) (string, error) {
|
|
nonce, err := randomHex(16)
|
|
if err != nil {
|
|
return "", fmt.Errorf("nonce: %w", err)
|
|
}
|
|
|
|
body, _ := json.Marshal(map[string]string{
|
|
"url": rawURL,
|
|
"method": method,
|
|
"nonce": nonce,
|
|
})
|
|
|
|
resp, err := http.Post(c.baseURL+"/auth/get-token", "application/json", bytes.NewReader(body))
|
|
if err != nil {
|
|
return "", fmt.Errorf("phone unreachable (%s): %w", c.baseURL, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
var result struct {
|
|
Ok bool `json:"ok"`
|
|
Token string `json:"token"`
|
|
Error string `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
return "", fmt.Errorf("parse token response: %w (body: %s)", err, raw)
|
|
}
|
|
if !result.Ok {
|
|
return "", fmt.Errorf("auth failed: %s", result.Error)
|
|
}
|
|
if result.Token == "" {
|
|
return "", fmt.Errorf("phone returned empty token")
|
|
}
|
|
return result.Token, nil
|
|
}
|
|
|
|
func randomHex(n int) (string, error) {
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(b), nil
|
|
}
|