package authentik import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) type Client struct { baseURL string token string httpClient *http.Client } func NewClient(baseURL, token string) *Client { return &Client{ baseURL: strings.TrimRight(baseURL, "/"), token: token, httpClient: &http.Client{ Timeout: 30 * time.Second, }, } } type listResponse[T any] struct { Results []T `json:"results"` } type flowRef struct { PK string `json:"pk"` Slug string `json:"slug"` } type oauth2Provider struct { PK int `json:"pk"` Name string `json:"name"` ClientID string `json:"client_id"` RedirectURIs string `json:"redirect_uris"` } type application struct { PK int `json:"pk"` Slug string `json:"slug"` Name string `json:"name"` Provider int `json:"provider"` } type scopeMapping struct { PK string `json:"pk"` ScopeName string `json:"scope_name"` } type certKeyPair struct { PK string `json:"pk"` Name string `json:"name"` } func (c *Client) Ping(ctx context.Context) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/api/v3/root/config/", nil) if err != nil { return err } c.authorize(req) resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("authentik ping: %d", resp.StatusCode) } return nil } func (c *Client) authorize(req *http.Request) { if c.token != "" { req.Header.Set("Authorization", "Bearer "+c.token) } req.Header.Set("Accept", "application/json") } func (c *Client) FindApplicationBySlug(ctx context.Context, slug string) (*application, bool, error) { q := url.Values{} q.Set("slug", slug) var out listResponse[application] if err := c.getJSON(ctx, "/api/v3/core/applications/?"+q.Encode(), &out); err != nil { return nil, false, err } if len(out.Results) == 0 { return nil, false, nil } return &out.Results[0], true, nil } func (c *Client) FindOAuth2ProviderByName(ctx context.Context, name string) (*oauth2Provider, bool, error) { q := url.Values{} q.Set("name", name) var out listResponse[oauth2Provider] if err := c.getJSON(ctx, "/api/v3/providers/oauth2/?"+q.Encode(), &out); err != nil { return nil, false, err } if len(out.Results) == 0 { return nil, false, nil } return &out.Results[0], true, nil } func (c *Client) FindFlowBySlug(ctx context.Context, slug string) (string, error) { q := url.Values{} q.Set("slug", slug) var out listResponse[flowRef] if err := c.getJSON(ctx, "/api/v3/flows/instances/?"+q.Encode(), &out); err != nil { return "", err } if len(out.Results) == 0 { return "", fmt.Errorf("flow not found: %s", slug) } return out.Results[0].PK, nil } func (c *Client) FindScopeMappingPK(ctx context.Context, scopeName string) (string, error) { q := url.Values{} q.Set("scope_name", scopeName) var out listResponse[scopeMapping] if err := c.getJSON(ctx, "/api/v3/propertymappings/provider/oauth2/?"+q.Encode(), &out); err != nil { return "", err } if len(out.Results) == 0 { return "", fmt.Errorf("scope mapping not found: %s", scopeName) } return out.Results[0].PK, nil } func (c *Client) FindSigningKeyPK(ctx context.Context) (string, error) { var out listResponse[certKeyPair] if err := c.getJSON(ctx, "/api/v3/crypto/certificatekeypairs/", &out); err != nil { return "", err } for _, k := range out.Results { if strings.Contains(k.Name, "authentik") && strings.Contains(k.Name, "Certificate") { return k.PK, nil } } if len(out.Results) > 0 { return out.Results[0].PK, nil } return "", fmt.Errorf("no signing key found") } type CreateOAuth2ProviderRequest struct { Name string ClientID string ClientSecret string RedirectURIs []string AuthorizationFlowPK string InvalidationFlowPK string SigningKeyPK string PropertyMappingPKs []string OfflineAccess bool } func (c *Client) CreateOAuth2Provider(ctx context.Context, req CreateOAuth2ProviderRequest) (int, error) { mappings := req.PropertyMappingPKs if req.OfflineAccess { if pk, err := c.FindScopeMappingPK(ctx, "offline_access"); err == nil { mappings = append(mappings, pk) } } body := map[string]any{ "name": req.Name, "authorization_flow": req.AuthorizationFlowPK, "invalidation_flow": req.InvalidationFlowPK, "property_mappings": mappings, "client_type": "confidential", "client_id": req.ClientID, "client_secret": req.ClientSecret, "redirect_uris": joinRedirectURIs(req.RedirectURIs), "signing_key": req.SigningKeyPK, "access_token_validity": "hours=1", "refresh_token_validity": "days=365", } var created oauth2Provider if err := c.postJSON(ctx, "/api/v3/providers/oauth2/", body, &created); err != nil { return 0, err } return created.PK, nil } func (c *Client) UpdateOAuth2ProviderRedirects(ctx context.Context, providerPK int, redirectURIs []string) error { body := map[string]any{ "redirect_uris": joinRedirectURIs(redirectURIs), } return c.patchJSON(ctx, fmt.Sprintf("/api/v3/providers/oauth2/%d/", providerPK), body) } type CreateApplicationRequest struct { Name string Slug string Group string LaunchURL string Provider int } func (c *Client) CreateApplication(ctx context.Context, req CreateApplicationRequest) (int, error) { body := map[string]any{ "name": req.Name, "slug": req.Slug, "group": req.Group, "provider": req.Provider, "meta_launch_url": req.LaunchURL, "policy_engine_mode": "any", } var created application if err := c.postJSON(ctx, "/api/v3/core/applications/", body, &created); err != nil { return 0, err } return created.PK, nil } func joinRedirectURIs(uris []string) string { seen := make(map[string]struct{}, len(uris)) var lines []string for _, u := range uris { u = strings.TrimSpace(u) if u == "" { continue } if _, ok := seen[u]; ok { continue } seen[u] = struct{}{} lines = append(lines, u) } return strings.Join(lines, "\n") } func (c *Client) getJSON(ctx context.Context, path string, dest any) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) if err != nil { return err } c.authorize(req) resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() return decodeResponse(resp, dest) } func (c *Client) postJSON(ctx context.Context, path string, body any, dest any) error { return c.doJSON(ctx, http.MethodPost, path, body, dest) } func (c *Client) patchJSON(ctx context.Context, path string, body any) error { return c.doJSON(ctx, http.MethodPatch, path, body, nil) } func (c *Client) doJSON(ctx context.Context, method, path string, body any, dest any) error { var reader io.Reader if body != nil { raw, err := json.Marshal(body) if err != nil { return err } reader = bytes.NewReader(raw) } req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reader) if err != nil { return err } c.authorize(req) req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() return decodeResponse(resp, dest) } func decodeResponse(resp *http.Response, dest any) error { if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) return fmt.Errorf("authentik api %s: %d %s", resp.Request.URL.Path, resp.StatusCode, strings.TrimSpace(string(b))) } if dest == nil { return nil } return json.NewDecoder(resp.Body).Decode(dest) }