1package server
2
3import (
4 "errors"
5 "time"
6
7 "github.com/Azure/go-autorest/autorest/to"
8 "github.com/haileyok/cocoon/internal/helpers"
9 "github.com/haileyok/cocoon/oauth"
10 "github.com/haileyok/cocoon/oauth/constants"
11 "github.com/haileyok/cocoon/oauth/dpop"
12 "github.com/haileyok/cocoon/oauth/provider"
13 "github.com/labstack/echo/v4"
14)
15
16type OauthParResponse struct {
17 ExpiresIn int64 `json:"expires_in"`
18 RequestURI string `json:"request_uri"`
19}
20
21func (s *Server) handleOauthPar(e echo.Context) error {
22 ctx := e.Request().Context()
23 logger := s.logger.With("name", "handleOauthPar")
24
25 var parRequest provider.ParRequest
26 if err := e.Bind(&parRequest); err != nil {
27 logger.Error("error binding for par request", "error", err)
28 return helpers.ServerError(e, nil)
29 }
30
31 if err := e.Validate(parRequest); err != nil {
32 logger.Error("missing parameters for par request", "error", err)
33 return helpers.InputError(e, nil)
34 }
35
36 // TODO: this seems wrong. should be a way to get the entire request url i believe, but this will work for now
37 dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil)
38 if err != nil {
39 if errors.Is(err, dpop.ErrUseDpopNonce) {
40 nonce := s.oauthProvider.NextNonce()
41 if nonce != "" {
42 e.Response().Header().Set("DPoP-Nonce", nonce)
43 e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce")
44 }
45 logger.Error("nonce error: use_dpop_nonce", "headers", e.Request().Header)
46 return e.JSON(400, map[string]string{
47 "error": "use_dpop_nonce",
48 })
49 }
50 logger.Error("error getting dpop proof", "error", err)
51 return helpers.InputError(e, nil)
52 }
53
54 client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), parRequest.AuthenticateClientRequestBase, dpopProof, &provider.AuthenticateClientOptions{
55 // rfc9449
56 // https://github.com/bluesky-social/atproto/blob/main/packages/oauth/oauth-provider/src/oauth-provider.ts#L473
57 AllowMissingDpopProof: true,
58 })
59 if err != nil {
60 logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err)
61 return helpers.InputError(e, to.StringPtr(err.Error()))
62 }
63
64 if parRequest.DpopJkt == nil {
65 if client.Metadata.DpopBoundAccessTokens {
66 parRequest.DpopJkt = to.StringPtr(dpopProof.JKT)
67 }
68 } else {
69 if !client.Metadata.DpopBoundAccessTokens {
70 msg := "dpop bound access tokens are not enabled for this client"
71 logger.Error(msg)
72 return helpers.InputError(e, &msg)
73 }
74
75 if dpopProof.JKT != *parRequest.DpopJkt {
76 msg := "supplied dpop jkt does not match header dpop jkt"
77 logger.Error(msg)
78 return helpers.InputError(e, &msg)
79 }
80 }
81
82 eat := time.Now().Add(constants.ParExpiresIn)
83 id := oauth.GenerateRequestId()
84
85 authRequest := &provider.OauthAuthorizationRequest{
86 RequestId: id,
87 ClientId: client.Metadata.ClientID,
88 ClientAuth: *clientAuth,
89 Parameters: parRequest,
90 ExpiresAt: eat,
91 }
92
93 if err := s.db.Create(ctx, authRequest, nil).Error; err != nil {
94 logger.Error("error creating auth request in db", "error", err)
95 return helpers.ServerError(e, nil)
96 }
97
98 uri := oauth.EncodeRequestUri(id)
99
100 return e.JSON(201, OauthParResponse{
101 ExpiresIn: int64(constants.ParExpiresIn.Seconds()),
102 RequestURI: uri,
103 })
104}