package agentidp import ( "bytes" "context" "encoding/json" "io" "net/http" "net/url" "strings" ) // TokenServiceClient provides token introspection and revocation. // Token acquisition is handled separately by TokenManager. type TokenServiceClient struct { baseURL string getToken func(ctx context.Context) (string, error) httpClient *http.Client } func newTokenServiceClient(baseURL string, getToken func(ctx context.Context) (string, error), httpClient *http.Client) *TokenServiceClient { return &TokenServiceClient{ baseURL: strings.TrimRight(baseURL, "/"), getToken: getToken, httpClient: httpClient, } } // IntrospectToken introspects an access token per RFC 7662. // POST /api/v1/token/introspect (form-encoded) → 200 IntrospectResponse func (c *TokenServiceClient) IntrospectToken(ctx context.Context, accessToken string) (*IntrospectResponse, error) { bearerToken, err := c.getToken(ctx) if err != nil { return nil, err } form := url.Values{} form.Set("token", accessToken) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/v1/token/introspect", bytes.NewBufferString(form.Encode())) if err != nil { return nil, &AgentIdPError{Code: "REQUEST_BUILD_ERROR", Message: "failed to build introspect request: " + err.Error()} } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", "Bearer "+bearerToken) resp, err := c.httpClient.Do(req) if err != nil { return nil, newNetworkError(err) } defer resp.Body.Close() //nolint:errcheck respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, newNetworkError(err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, parseAPIError(respBody, resp.StatusCode) } var result IntrospectResponse if err := json.Unmarshal(respBody, &result); err != nil { return nil, &AgentIdPError{Code: "PARSE_ERROR", Message: "failed to parse introspect response: " + err.Error(), HTTPStatus: resp.StatusCode} } return &result, nil } // RevokeToken revokes an access token. // POST /api/v1/token/revoke (form-encoded) → 200 func (c *TokenServiceClient) RevokeToken(ctx context.Context, accessToken string) error { bearerToken, err := c.getToken(ctx) if err != nil { return err } form := url.Values{} form.Set("token", accessToken) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/v1/token/revoke", bytes.NewBufferString(form.Encode())) if err != nil { return &AgentIdPError{Code: "REQUEST_BUILD_ERROR", Message: "failed to build revoke request: " + err.Error()} } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Authorization", "Bearer "+bearerToken) resp, err := c.httpClient.Do(req) if err != nil { return newNetworkError(err) } defer resp.Body.Close() //nolint:errcheck respBody, err := io.ReadAll(resp.Body) if err != nil { return newNetworkError(err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return parseAPIError(respBody, resp.StatusCode) } return nil }