1package server
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/Azure/go-autorest/autorest/to"
8 "github.com/haileyok/cocoon/internal/helpers"
9 "github.com/labstack/echo/v4"
10 "gorm.io/gorm"
11)
12
13var (
14 CocoonSupportedScopes = []string{
15 "atproto",
16 "transition:email",
17 "transition:generic",
18 "transition:chat.bsky",
19 }
20)
21
22type OauthAuthorizationMetadata struct {
23 Issuer string `json:"issuer"`
24 RequestParameterSupported bool `json:"request_parameter_supported"`
25 RequestUriParameterSupported bool `json:"request_uri_parameter_supported"`
26 RequireRequestUriRegistration *bool `json:"require_request_uri_registration,omitempty"`
27 ScopesSupported []string `json:"scopes_supported"`
28 SubjectTypesSupported []string `json:"subject_types_supported"`
29 ResponseTypesSupported []string `json:"response_types_supported"`
30 ResponseModesSupported []string `json:"response_modes_supported"`
31 GrantTypesSupported []string `json:"grant_types_supported"`
32 CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
33 UILocalesSupported []string `json:"ui_locales_supported"`
34 DisplayValuesSupported []string `json:"display_values_supported"`
35 RequestObjectSigningAlgValuesSupported []string `json:"request_object_signing_alg_values_supported"`
36 AuthorizationResponseISSParameterSupported bool `json:"authorization_response_iss_parameter_supported"`
37 RequestObjectEncryptionAlgValuesSupported []string `json:"request_object_encryption_alg_values_supported"`
38 RequestObjectEncryptionEncValuesSupported []string `json:"request_object_encryption_enc_values_supported"`
39 JwksUri string `json:"jwks_uri"`
40 AuthorizationEndpoint string `json:"authorization_endpoint"`
41 TokenEndpoint string `json:"token_endpoint"`
42 TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
43 TokenEndpointAuthSigningAlgValuesSupported []string `json:"token_endpoint_auth_signing_alg_values_supported"`
44 RevocationEndpoint string `json:"revocation_endpoint"`
45 IntrospectionEndpoint string `json:"introspection_endpoint"`
46 PushedAuthorizationRequestEndpoint string `json:"pushed_authorization_request_endpoint"`
47 RequirePushedAuthorizationRequests bool `json:"require_pushed_authorization_requests"`
48 DpopSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported"`
49 ProtectedResources []string `json:"protected_resources"`
50 ClientIDMetadataDocumentSupported bool `json:"client_id_metadata_document_supported"`
51}
52
53func (s *Server) handleWellKnown(e echo.Context) error {
54 return e.JSON(200, map[string]any{
55 "@context": []string{
56 "https://www.w3.org/ns/did/v1",
57 },
58 "id": s.config.Did,
59 "service": []map[string]string{
60 {
61 "id": "#atproto_pds",
62 "type": "AtprotoPersonalDataServer",
63 "serviceEndpoint": "https://" + s.config.Hostname,
64 },
65 },
66 })
67}
68
69func (s *Server) handleAtprotoDid(e echo.Context) error {
70 ctx := e.Request().Context()
71 logger := s.logger.With("name", "handleAtprotoDid")
72
73 host := e.Request().Host
74 if host == "" {
75 return helpers.InputError(e, to.StringPtr("Invalid handle."))
76 }
77
78 host = strings.Split(host, ":")[0]
79 host = strings.ToLower(strings.TrimSpace(host))
80
81 if host == s.config.Hostname {
82 return e.String(200, s.config.Did)
83 }
84
85 suffix := "." + s.config.Hostname
86 if !strings.HasSuffix(host, suffix) {
87 return e.NoContent(404)
88 }
89
90 actor, err := s.getActorByHandle(ctx, host)
91 if err != nil {
92 if err == gorm.ErrRecordNotFound {
93 return e.NoContent(404)
94 }
95 logger.Error("error looking up actor by handle", "error", err)
96 return helpers.ServerError(e, nil)
97 }
98
99 return e.String(200, actor.Did)
100}
101
102func (s *Server) handleOauthProtectedResource(e echo.Context) error {
103 return e.JSON(200, map[string]any{
104 "resource": "https://" + s.config.Hostname,
105 "authorization_servers": []string{
106 "https://" + s.config.Hostname,
107 },
108 "scopes_supported": []string{},
109 "bearer_methods_supported": []string{"header"},
110 "resource_documentation": "https://atproto.com",
111 })
112}
113
114func (s *Server) handleOauthAuthorizationServer(e echo.Context) error {
115 return e.JSON(200, OauthAuthorizationMetadata{
116 Issuer: "https://" + s.config.Hostname,
117 RequestParameterSupported: true,
118 RequestUriParameterSupported: true,
119 RequireRequestUriRegistration: to.BoolPtr(true),
120 ScopesSupported: CocoonSupportedScopes,
121 SubjectTypesSupported: []string{"public"},
122 ResponseTypesSupported: []string{"code"},
123 ResponseModesSupported: []string{"query", "fragment", "form_post"},
124 GrantTypesSupported: []string{"authorization_code", "refresh_token"},
125 CodeChallengeMethodsSupported: []string{"S256"},
126 UILocalesSupported: []string{"en-US"},
127 DisplayValuesSupported: []string{"page", "popup", "touch"},
128 RequestObjectSigningAlgValuesSupported: []string{"ES256"}, // only es256 for now...
129 AuthorizationResponseISSParameterSupported: true,
130 RequestObjectEncryptionAlgValuesSupported: []string{},
131 RequestObjectEncryptionEncValuesSupported: []string{},
132 JwksUri: fmt.Sprintf("https://%s/oauth/jwks", s.config.Hostname),
133 AuthorizationEndpoint: fmt.Sprintf("https://%s/oauth/authorize", s.config.Hostname),
134 TokenEndpoint: fmt.Sprintf("https://%s/oauth/token", s.config.Hostname),
135 TokenEndpointAuthMethodsSupported: []string{"none", "private_key_jwt"},
136 TokenEndpointAuthSigningAlgValuesSupported: []string{"ES256"}, // Same as above, just es256
137 RevocationEndpoint: fmt.Sprintf("https://%s/oauth/revoke", s.config.Hostname),
138 IntrospectionEndpoint: fmt.Sprintf("https://%s/oauth/introspect", s.config.Hostname),
139 PushedAuthorizationRequestEndpoint: fmt.Sprintf("https://%s/oauth/par", s.config.Hostname),
140 RequirePushedAuthorizationRequests: true,
141 DpopSigningAlgValuesSupported: []string{"ES256"}, // again same as above
142 ProtectedResources: []string{"https://" + s.config.Hostname},
143 ClientIDMetadataDocumentSupported: true,
144 })
145}