forked from hailey.at/cocoon
An atproto PDS written in Go
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}