ory-am/hydra

View on GitHub
client/handler.go

Summary

Maintainability
A
2 hrs
Test Coverage
// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0

package client

import (
    "context"
    "crypto/subtle"
    "encoding/json"
    "io"
    "net/http"
    "strings"
    "time"

    "github.com/julienschmidt/httprouter"
    "github.com/pkg/errors"

    "github.com/ory/fosite"
    "github.com/ory/herodot"
    "github.com/ory/hydra/v2/x"
    "github.com/ory/x/errorsx"
    "github.com/ory/x/httprouterx"
    "github.com/ory/x/jsonx"
    "github.com/ory/x/openapix"
    "github.com/ory/x/pagination/tokenpagination"
    "github.com/ory/x/urlx"
    "github.com/ory/x/uuidx"
)

type Handler struct {
    r InternalRegistry
}

const (
    ClientsHandlerPath    = "/clients"
    DynClientsHandlerPath = "/oauth2/register"
)

func NewHandler(r InternalRegistry) *Handler {
    return &Handler{
        r: r,
    }
}

func (h *Handler) SetRoutes(admin *httprouterx.RouterAdmin, public *httprouterx.RouterPublic) {
    admin.GET(ClientsHandlerPath, h.listOAuth2Clients)
    admin.POST(ClientsHandlerPath, h.createOAuth2Client)
    admin.GET(ClientsHandlerPath+"/:id", h.Get)
    admin.PUT(ClientsHandlerPath+"/:id", h.setOAuth2Client)
    admin.PATCH(ClientsHandlerPath+"/:id", h.patchOAuth2Client)
    admin.DELETE(ClientsHandlerPath+"/:id", h.deleteOAuth2Client)
    admin.PUT(ClientsHandlerPath+"/:id/lifespans", h.setOAuth2ClientLifespans)

    public.POST(DynClientsHandlerPath, h.createOidcDynamicClient)
    public.GET(DynClientsHandlerPath+"/:id", h.getOidcDynamicClient)
    public.PUT(DynClientsHandlerPath+"/:id", h.setOidcDynamicClient)
    public.DELETE(DynClientsHandlerPath+"/:id", h.deleteOidcDynamicClient)
}

// OAuth 2.0 Client Creation Parameters
//
// swagger:parameters createOAuth2Client
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type createOAuth2Client struct {
    // OAuth 2.0 Client Request Body
    //
    // in: body
    // required: true
    Body Client
}

// swagger:route POST /admin/clients oAuth2 createOAuth2Client
//
// # Create OAuth 2.0 Client
//
// Create a new OAuth 2.0 client. If you pass `client_secret` the secret is used, otherwise a random secret
// is generated. The secret is echoed in the response. It is not possible to retrieve it later on.
//
//    Consumes:
//    - application/json
//
//    Produces:
//    - application/json
//
//    Schemes: http, https
//
//    Responses:
//      201: oAuth2Client
//      400: errorOAuth2BadRequest
//      default: errorOAuth2Default
func (h *Handler) createOAuth2Client(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    c, err := h.CreateClient(r, h.r.ClientValidator().Validate, false)
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    h.r.Writer().WriteCreated(w, r, "/admin"+ClientsHandlerPath+"/"+c.GetID(), &c)
}

// OpenID Connect Dynamic Client Registration Parameters
//
// swagger:parameters createOidcDynamicClient
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type createOidcDynamicClient struct {
    // Dynamic Client Registration Request Body
    //
    // in: body
    // required: true
    Body Client
}

// swagger:route POST /oauth2/register oidc createOidcDynamicClient
//
// # Register OAuth2 Client using OpenID Dynamic Client Registration
//
// This endpoint behaves like the administrative counterpart (`createOAuth2Client`) but is capable of facing the
// public internet directly and can be used in self-service. It implements the OpenID Connect
// Dynamic Client Registration Protocol. This feature needs to be enabled in the configuration. This endpoint
// is disabled by default. It can be enabled by an administrator.
//
// Please note that using this endpoint you are not able to choose the `client_secret` nor the `client_id` as those
// values will be server generated when specifying `token_endpoint_auth_method` as `client_secret_basic` or
// `client_secret_post`.
//
// The `client_secret` will be returned in the response and you will not be able to retrieve it later on.
// Write the secret down and keep it somewhere safe.
//
//    Consumes:
//    - application/json
//
//    Produces:
//    - application/json
//
//    Schemes: http, https
//
//    Responses:
//      201: oAuth2Client
//      400: errorOAuth2BadRequest
//      default: errorOAuth2Default
func (h *Handler) createOidcDynamicClient(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    if err := h.requireDynamicAuth(r); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }
    c, err := h.CreateClient(r, h.r.ClientValidator().ValidateDynamicRegistration, true)
    if err != nil {
        h.r.Writer().WriteError(w, r, errorsx.WithStack(err))
        return
    }

    h.r.Writer().WriteCreated(w, r, "/admin"+ClientsHandlerPath+"/"+c.GetID(), &c)
}

func (h *Handler) CreateClient(r *http.Request, validator func(context.Context, *Client) error, isDynamic bool) (*Client, error) {
    var c Client
    if err := json.NewDecoder(r.Body).Decode(&c); err != nil {
        return nil, errorsx.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to decode the request body: %s", err))
    }

    if isDynamic {
        if c.Secret != "" {
            return nil, errorsx.WithStack(herodot.ErrBadRequest.WithReasonf("It is not allowed to choose your own OAuth2 Client secret."))
        }
        // We do not allow to set the client ID for dynamic clients.
        c.ID = uuidx.NewV4().String()
    }

    if len(c.Secret) == 0 {
        secretb, err := x.GenerateSecret(26)
        if err != nil {
            return nil, err
        }
        c.Secret = string(secretb)
    }

    if err := validator(r.Context(), &c); err != nil {
        return nil, err
    }

    secret := c.Secret
    c.CreatedAt = time.Now().UTC().Round(time.Second)
    c.UpdatedAt = c.CreatedAt

    token, signature, err := h.r.OAuth2HMACStrategy().GenerateAccessToken(r.Context(), nil)
    if err != nil {
        return nil, err
    }

    c.RegistrationAccessToken = token
    c.RegistrationAccessTokenSignature = signature
    c.RegistrationClientURI = urlx.AppendPaths(h.r.Config().PublicURL(r.Context()), DynClientsHandlerPath+"/"+c.GetID()).String()

    if err := h.r.ClientManager().CreateClient(r.Context(), &c); err != nil {
        return nil, err
    }
    c.Secret = ""
    if !c.IsPublic() {
        c.Secret = secret
    }
    return &c, nil
}

// Set OAuth 2.0 Client Parameters
//
// swagger:parameters setOAuth2Client
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type setOAuth2Client struct {
    // OAuth 2.0 Client ID
    //
    // in: path
    // required: true
    ID string `json:"id"`

    // OAuth 2.0 Client Request Body
    //
    // in: body
    // required: true
    Body Client
}

// swagger:route PUT /admin/clients/{id} oAuth2 setOAuth2Client
//
// # Set OAuth 2.0 Client
//
// Replaces an existing OAuth 2.0 Client with the payload you send. If you pass `client_secret` the secret is used,
// otherwise the existing secret is used.
//
// If set, the secret is echoed in the response. It is not possible to retrieve it later on.
//
// OAuth 2.0 Clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are
// generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities.
//
//    Consumes:
//    - application/json
//
//    Produces:
//    - application/json
//
//    Schemes: http, https
//
//    Responses:
//      200: oAuth2Client
//      400: errorOAuth2BadRequest
//      404: errorOAuth2NotFound
//      default: errorOAuth2Default
func (h *Handler) setOAuth2Client(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    var c Client
    if err := json.NewDecoder(r.Body).Decode(&c); err != nil {
        h.r.Writer().WriteError(w, r, errorsx.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to decode the request body: %s", err)))
        return
    }

    c.ID = ps.ByName("id")
    if err := h.updateClient(r.Context(), &c, h.r.ClientValidator().Validate); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    h.r.Writer().Write(w, r, &c)
}

func (h *Handler) updateClient(ctx context.Context, c *Client, validator func(context.Context, *Client) error) error {
    var secret string
    if len(c.Secret) > 0 {
        secret = c.Secret
    }

    if err := validator(ctx, c); err != nil {
        return err
    }

    c.UpdatedAt = time.Now().UTC().Round(time.Second)
    if err := h.r.ClientManager().UpdateClient(ctx, c); err != nil {
        return err
    }
    c.Secret = secret
    return nil
}

// Set Dynamic Client Parameters
//
// swagger:parameters setOidcDynamicClient
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type setOidcDynamicClient struct {
    // OAuth 2.0 Client ID
    //
    // in: path
    // required: true
    ID string `json:"id"`

    // OAuth 2.0 Client Request Body
    //
    // in: body
    // required: true
    Body Client
}

// swagger:route PUT /oauth2/register/{id} oidc setOidcDynamicClient
//
// # Set OAuth2 Client using OpenID Dynamic Client Registration
//
// This endpoint behaves like the administrative counterpart (`setOAuth2Client`) but is capable of facing the
// public internet directly to be used by third parties. It implements the OpenID Connect
// Dynamic Client Registration Protocol.
//
// This feature is disabled per default. It can be enabled by a system administrator.
//
// If you pass `client_secret` the secret is used, otherwise the existing secret is used. If set, the secret is echoed in the response.
// It is not possible to retrieve it later on.
//
// To use this endpoint, you will need to present the client's authentication credentials. If the OAuth2 Client
// uses the Token Endpoint Authentication Method `client_secret_post`, you need to present the client secret in the URL query.
// If it uses `client_secret_basic`, present the Client ID and the Client Secret in the Authorization header.
//
// OAuth 2.0 clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are
// generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities.
//
//    Consumes:
//    - application/json
//
//    Produces:
//    - application/json
//
//    Security:
//      bearer:
//
//    Schemes: http, https
//
//    Responses:
//      200: oAuth2Client
//      404: errorOAuth2NotFound
//      default: errorOAuth2Default
func (h *Handler) setOidcDynamicClient(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    if err := h.requireDynamicAuth(r); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    client, err := h.ValidDynamicAuth(r, ps)
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    var c Client
    if err := json.NewDecoder(r.Body).Decode(&c); err != nil {
        h.r.Writer().WriteError(w, r, errorsx.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to decode the request body. Is it valid JSON?").WithDebug(err.Error())))
        return
    }

    if c.Secret != "" {
        h.r.Writer().WriteError(w, r, errorsx.WithStack(herodot.ErrForbidden.WithReasonf("It is not allowed to choose your own OAuth2 Client secret.")))
        return
    }

    // Regenerate the registration access token
    token, signature, err := h.r.OAuth2HMACStrategy().GenerateAccessToken(r.Context(), nil)
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }
    c.RegistrationAccessToken = token
    c.RegistrationAccessTokenSignature = signature

    c.ID = client.GetID()
    if err := h.updateClient(r.Context(), &c, h.r.ClientValidator().ValidateDynamicRegistration); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    h.r.Writer().Write(w, r, &c)
}

// Patch OAuth 2.0 Client Parameters
//
// swagger:parameters patchOAuth2Client
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type patchOAuth2Client struct {
    // The id of the OAuth 2.0 Client.
    //
    // in: path
    // required: true
    ID string `json:"id"`

    // OAuth 2.0 Client JSON Patch Body
    //
    // in: body
    // required: true
    Body openapix.JSONPatchDocument
}

// swagger:route PATCH /admin/clients/{id} oAuth2 patchOAuth2Client
//
// # Patch OAuth 2.0 Client
//
// Patch an existing OAuth 2.0 Client using JSON Patch. If you pass `client_secret`
// the secret will be updated and returned via the API. This is the
// only time you will be able to retrieve the client secret, so write it down and keep it safe.
//
// OAuth 2.0 clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are
// generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities.
//
//    Consumes:
//    - application/json
//
//    Produces:
//    - application/json
//
//    Schemes: http, https
//
//    Responses:
//      200: oAuth2Client
//      404: errorOAuth2NotFound
//      default: errorOAuth2Default
func (h *Handler) patchOAuth2Client(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    patchJSON, err := io.ReadAll(r.Body)
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    id := ps.ByName("id")
    c, err := h.r.ClientManager().GetConcreteClient(r.Context(), id)
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    oldSecret := c.Secret

    if err := jsonx.ApplyJSONPatch(patchJSON, c, "/id"); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    // fix for #2869
    // GetConcreteClient returns a client with the hashed secret, however updateClient expects
    // an empty secret if the secret hasn't changed. As such we need to check if the patch has
    // updated the secret or not
    if oldSecret == c.Secret {
        c.Secret = ""
    }

    if err := h.updateClient(r.Context(), c, h.r.ClientValidator().Validate); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    h.r.Writer().Write(w, r, c)
}

// Paginated OAuth2 Client List Response
//
// swagger:response listOAuth2Clients
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type listOAuth2ClientsResponse struct {
    tokenpagination.ResponseHeaders

    // List of OAuth 2.0 Clients
    //
    // in:body
    Body []Client
}

// Paginated OAuth2 Client List Parameters
//
// swagger:parameters listOAuth2Clients
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type listOAuth2ClientsParameters struct {
    tokenpagination.RequestParameters

    // The name of the clients to filter by.
    //
    // in: query
    Name string `json:"client_name"`

    // The owner of the clients to filter by.
    //
    // in: query
    Owner string `json:"owner"`
}

// swagger:route GET /admin/clients oAuth2 listOAuth2Clients
//
// # List OAuth 2.0 Clients
//
// This endpoint lists all clients in the database, and never returns client secrets.
// As a default it lists the first 100 clients.
//
//    Consumes:
//    - application/json
//
//    Produces:
//    - application/json
//
//    Schemes: http, https
//
//    Responses:
//      200: listOAuth2Clients
//      default: errorOAuth2Default
func (h *Handler) listOAuth2Clients(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    page, itemsPerPage := x.ParsePagination(r)
    filters := Filter{
        Limit:  itemsPerPage,
        Offset: page * itemsPerPage,
        Name:   r.URL.Query().Get("client_name"),
        Owner:  r.URL.Query().Get("owner"),
    }

    c, err := h.r.ClientManager().GetClients(r.Context(), filters)
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    if c == nil {
        c = []Client{}
    }

    for k := range c {
        c[k].Secret = ""
    }

    total, err := h.r.ClientManager().CountClients(r.Context())
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    x.PaginationHeader(w, r.URL, int64(total), page, itemsPerPage)
    h.r.Writer().Write(w, r, c)
}

// Get OAuth2 Client Parameters
//
// swagger:parameters getOAuth2Client
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type adminGetOAuth2Client struct {
    // The id of the OAuth 2.0 Client.
    //
    // in: path
    // required: true
    ID string `json:"id"`
}

// swagger:route GET /admin/clients/{id} oAuth2 getOAuth2Client
//
// # Get an OAuth 2.0 Client
//
// Get an OAuth 2.0 client by its ID. This endpoint never returns the client secret.
//
// OAuth 2.0 clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are
// generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities.
//
//    Consumes:
//    - application/json
//
//    Produces:
//    - application/json
//
//    Schemes: http, https
//
//    Responses:
//      200: oAuth2Client
//      default: errorOAuth2Default
func (h *Handler) Get(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    var id = ps.ByName("id")
    c, err := h.r.ClientManager().GetConcreteClient(r.Context(), id)
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    c.Secret = ""
    h.r.Writer().Write(w, r, c)
}

// Get OpenID Connect Dynamic Client Parameters
//
// swagger:parameters getOidcDynamicClient
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type getOidcDynamicClient struct {
    // The id of the OAuth 2.0 Client.
    //
    // in: path
    // required: true
    ID string `json:"id"`
}

// swagger:route GET /oauth2/register/{id} oidc getOidcDynamicClient
//
// # Get OAuth2 Client using OpenID Dynamic Client Registration
//
// This endpoint behaves like the administrative counterpart (`getOAuth2Client`) but is capable of facing the
// public internet directly and can be used in self-service. It implements the OpenID Connect
// Dynamic Client Registration Protocol.
//
// To use this endpoint, you will need to present the client's authentication credentials. If the OAuth2 Client
// uses the Token Endpoint Authentication Method `client_secret_post`, you need to present the client secret in the URL query.
// If it uses `client_secret_basic`, present the Client ID and the Client Secret in the Authorization header.
//
//    Consumes:
//    - application/json
//
//    Produces:
//    - application/json
//
//    Schemes: http, https
//
//    Security:
//      bearer:
//
//    Responses:
//      200: oAuth2Client
//      default: errorOAuth2Default
func (h *Handler) getOidcDynamicClient(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    if err := h.requireDynamicAuth(r); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    client, err := h.ValidDynamicAuth(r, ps)
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    c, err := h.r.ClientManager().GetConcreteClient(r.Context(), client.GetID())
    if err != nil {
        err = herodot.ErrUnauthorized.WithReason("The requested OAuth 2.0 client does not exist or you did not provide the necessary credentials")
        h.r.Writer().WriteError(w, r, err)
        return
    }

    c.Secret = ""
    c.Metadata = nil
    h.r.Writer().Write(w, r, c)
}

// Delete OAuth2 Client Parameters
//
// swagger:parameters deleteOAuth2Client
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type deleteOAuth2Client struct {
    // The id of the OAuth 2.0 Client.
    //
    // in: path
    // required: true
    ID string `json:"id"`
}

// swagger:route DELETE /admin/clients/{id} oAuth2 deleteOAuth2Client
//
// # Delete OAuth 2.0 Client
//
// Delete an existing OAuth 2.0 Client by its ID.
//
// OAuth 2.0 clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are
// generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities.
//
// Make sure that this endpoint is well protected and only callable by first-party components.
//
//    Consumes:
//    - application/json
//
//    Produces:
//    - application/json
//
//    Schemes: http, https
//
//    Responses:
//      204: emptyResponse
//      default: genericError
func (h *Handler) deleteOAuth2Client(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    var id = ps.ByName("id")
    if err := h.r.ClientManager().DeleteClient(r.Context(), id); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

// Set OAuth 2.0 Client Token Lifespans
//
// swagger:parameters setOAuth2ClientLifespans
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type setOAuth2ClientLifespans struct {
    // OAuth 2.0 Client ID
    //
    // in: path
    // required: true
    ID string `json:"id"`

    // in: body
    Body Lifespans
}

// swagger:route PUT /admin/clients/{id}/lifespans oAuth2 setOAuth2ClientLifespans
//
// # Set OAuth2 Client Token Lifespans
//
// Set lifespans of different token types issued for this OAuth 2.0 client. Does not modify other fields.
//
//    Consumes:
//    - application/json
//
//    Schemes: http, https
//
//    Responses:
//      200: oAuth2Client
//      default: genericError
func (h *Handler) setOAuth2ClientLifespans(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    var id = ps.ByName("id")
    c, err := h.r.ClientManager().GetConcreteClient(r.Context(), id)
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    var ls Lifespans
    if err := json.NewDecoder(r.Body).Decode(&ls); err != nil {
        h.r.Writer().WriteError(w, r, errorsx.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to decode the request body: %s", err)))
        return
    }

    c.Lifespans = ls
    c.Secret = ""

    if err := h.updateClient(r.Context(), c, h.r.ClientValidator().Validate); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    h.r.Writer().Write(w, r, c)
}

// swagger:parameters deleteOidcDynamicClient
//
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type dynamicClientRegistrationDeleteOAuth2Client struct {
    // The id of the OAuth 2.0 Client.
    //
    // in: path
    // required: true
    ID string `json:"id"`
}

// swagger:route DELETE /oauth2/register/{id} oidc deleteOidcDynamicClient
//
// # Delete OAuth 2.0 Client using the OpenID Dynamic Client Registration Management Protocol
//
// This endpoint behaves like the administrative counterpart (`deleteOAuth2Client`) but is capable of facing the
// public internet directly and can be used in self-service. It implements the OpenID Connect
// Dynamic Client Registration Protocol. This feature needs to be enabled in the configuration. This endpoint
// is disabled by default. It can be enabled by an administrator.
//
// To use this endpoint, you will need to present the client's authentication credentials. If the OAuth2 Client
// uses the Token Endpoint Authentication Method `client_secret_post`, you need to present the client secret in the URL query.
// If it uses `client_secret_basic`, present the Client ID and the Client Secret in the Authorization header.
//
// OAuth 2.0 clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are
// generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities.
//
//    Produces:
//    - application/json
//
//    Schemes: http, https
//
//    Security:
//      bearer:
//
//    Responses:
//      204: emptyResponse
//      default: genericError
func (h *Handler) deleteOidcDynamicClient(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    if err := h.requireDynamicAuth(r); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }
    client, err := h.ValidDynamicAuth(r, ps)
    if err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    if err := h.r.ClientManager().DeleteClient(r.Context(), client.GetID()); err != nil {
        h.r.Writer().WriteError(w, r, err)
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) ValidDynamicAuth(r *http.Request, ps httprouter.Params) (fosite.Client, error) {
    c, err := h.r.ClientManager().GetConcreteClient(r.Context(), ps.ByName("id"))
    if err != nil {
        return nil, herodot.ErrUnauthorized.
            WithTrace(err).
            WithReason("The requested OAuth 2.0 client does not exist or you provided incorrect credentials.").WithDebug(err.Error())
    }

    if len(c.RegistrationAccessTokenSignature) == 0 {
        return nil, errors.WithStack(herodot.ErrUnauthorized.
            WithReason("The requested OAuth 2.0 client does not exist or you provided incorrect credentials.").WithDebug("The OAuth2 Client does not have a registration access token."))
    }

    token := strings.TrimPrefix(fosite.AccessTokenFromRequest(r), "ory_at_")
    if err := h.r.OAuth2HMACStrategy().ValidateAccessToken(
        r.Context(),
        // The strategy checks the expiry time of the token. Registration tokens don't expire (we don't have a way of
        // rotating them) so we set the expiry time to a time in the future.
        &fosite.Request{
            Session: &fosite.DefaultSession{
                ExpiresAt: map[fosite.TokenType]time.Time{
                    fosite.AccessToken: time.Now().Add(time.Hour),
                },
            },
            RequestedAt: time.Now(),
        },
        token,
    ); err != nil {
        return nil, herodot.ErrUnauthorized.
            WithTrace(err).
            WithReason("The requested OAuth 2.0 client does not exist or you provided incorrect credentials.").WithDebug(err.Error())
    }

    signature := h.r.OAuth2EnigmaStrategy().Signature(token)
    if subtle.ConstantTimeCompare([]byte(c.RegistrationAccessTokenSignature), []byte(signature)) == 0 {
        return nil, errors.WithStack(herodot.ErrUnauthorized.
            WithReason("The requested OAuth 2.0 client does not exist or you provided incorrect credentials.").WithDebug("Registration access tokens do not match."))
    }

    return c, nil
}

func (h *Handler) requireDynamicAuth(r *http.Request) *herodot.DefaultError {
    if !h.r.Config().PublicAllowDynamicRegistration(r.Context()) {
        return herodot.ErrNotFound.WithReason("Dynamic registration is not enabled.")
    }
    return nil
}