forked from hailey.at/cocoon
An atproto PDS written in Go
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 return e.JSON(400, map[string]string{ 46 "error": "use_dpop_nonce", 47 }) 48 } 49 logger.Error("error getting dpop proof", "error", err) 50 return helpers.InputError(e, nil) 51 } 52 53 client, clientAuth, err := s.oauthProvider.AuthenticateClient(e.Request().Context(), parRequest.AuthenticateClientRequestBase, dpopProof, &provider.AuthenticateClientOptions{ 54 // rfc9449 55 // https://github.com/bluesky-social/atproto/blob/main/packages/oauth/oauth-provider/src/oauth-provider.ts#L473 56 AllowMissingDpopProof: true, 57 }) 58 if err != nil { 59 logger.Error("error authenticating client", "client_id", parRequest.ClientID, "error", err) 60 return helpers.InputError(e, to.StringPtr(err.Error())) 61 } 62 63 if parRequest.DpopJkt == nil { 64 if client.Metadata.DpopBoundAccessTokens { 65 parRequest.DpopJkt = to.StringPtr(dpopProof.JKT) 66 } 67 } else { 68 if !client.Metadata.DpopBoundAccessTokens { 69 msg := "dpop bound access tokens are not enabled for this client" 70 logger.Error(msg) 71 return helpers.InputError(e, &msg) 72 } 73 74 if dpopProof.JKT != *parRequest.DpopJkt { 75 msg := "supplied dpop jkt does not match header dpop jkt" 76 logger.Error(msg) 77 return helpers.InputError(e, &msg) 78 } 79 } 80 81 eat := time.Now().Add(constants.ParExpiresIn) 82 id := oauth.GenerateRequestId() 83 84 authRequest := &provider.OauthAuthorizationRequest{ 85 RequestId: id, 86 ClientId: client.Metadata.ClientID, 87 ClientAuth: *clientAuth, 88 Parameters: parRequest, 89 ExpiresAt: eat, 90 } 91 92 if err := s.db.Create(ctx, authRequest, nil).Error; err != nil { 93 logger.Error("error creating auth request in db", "error", err) 94 return helpers.ServerError(e, nil) 95 } 96 97 uri := oauth.EncodeRequestUri(id) 98 99 return e.JSON(201, OauthParResponse{ 100 ExpiresIn: int64(constants.ParExpiresIn.Seconds()), 101 RequestURI: uri, 102 }) 103}