this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fill in more OAuth verification code

+109 -42
+14 -15
atproto/auth/oauth/oauth.go
··· 124 124 Config: config, 125 125 Data: data, 126 126 } 127 - priv, err := crypto.ParsePrivateMultibase(data.DpopKeyMultibase) 127 + priv, err := crypto.ParsePrivateMultibase(data.DpopPrivateKeyMultibase) 128 128 if err != nil { 129 129 return nil, err 130 130 } ··· 244 244 RedirectURI: c.CallbackURL, 245 245 Scope: scope, 246 246 ResponseType: "code", 247 - ClientAssertionType: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 247 + ClientAssertionType: CLIENT_ASSERTION_JWT_BEARER, 248 248 ClientAssertion: assertionJWT, 249 249 CodeChallenge: codeChallenge, 250 250 CodeChallengeMethod: "S256", ··· 323 323 } 324 324 325 325 parInfo := AuthRequestData{ 326 - State: state, 327 - AuthServerURL: authMeta.Issuer, 328 - //XXX: HostURL 329 - Scope: scope, 330 - PKCEVerifier: pkceVerifier, 331 - RequestURI: parResp.RequestURI, 332 - DpopAuthServerNonce: dpopServerNonce, 333 - DpopKeyMultibase: dpopPrivKey.Multibase(), 326 + State: state, 327 + AuthServerURL: authMeta.Issuer, 328 + Scope: scope, 329 + PKCEVerifier: pkceVerifier, 330 + RequestURI: parResp.RequestURI, 331 + DpopAuthServerNonce: dpopServerNonce, 332 + DpopPrivateKeyMultibase: dpopPrivKey.Multibase(), 334 333 } 335 334 336 335 return &parInfo, nil ··· 359 358 GrantType: "authorization_code", 360 359 Code: authCode, 361 360 CodeVerifier: info.PKCEVerifier, 362 - ClientAssertionType: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 363 - ClientAssertion: clientAssertion, 361 + ClientAssertionType: &CLIENT_ASSERTION_JWT_BEARER, 362 + ClientAssertion: &clientAssertion, 364 363 } 365 364 366 - dpopPrivKey, err := crypto.ParsePrivateMultibase(info.DpopKeyMultibase) 365 + dpopPrivKey, err := crypto.ParsePrivateMultibase(info.DpopPrivateKeyMultibase) 367 366 if err != nil { 368 367 return nil, err 369 368 } ··· 445 444 ClientID: sess.Config.ClientID, 446 445 GrantType: "authorization_code", 447 446 RefreshToken: sess.Data.RefreshToken, 448 - ClientAssertionType: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 449 - ClientAssertion: clientAssertion, 447 + ClientAssertionType: &CLIENT_ASSERTION_JWT_BEARER, 448 + ClientAssertion: &clientAssertion, 450 449 } 451 450 452 451 vals, err := query.Values(body)
+95 -27
atproto/auth/oauth/oauth_types.go
··· 5 5 "fmt" 6 6 "net/url" 7 7 "slices" 8 + "strings" 8 9 9 10 "github.com/bluesky-social/indigo/atproto/crypto" 10 11 "github.com/bluesky-social/indigo/atproto/syntax" 11 12 ) 12 13 13 - var ErrInvalidAuthServerMetadata = errors.New("invalid auth server metadata") 14 + var ( 15 + ErrInvalidAuthServerMetadata = errors.New("invalid auth server metadata") 16 + ErrInvalidClientMetadata = errors.New("invalid client metadata doc") 17 + CLIENT_ASSERTION_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 18 + ) 14 19 15 20 type JWKS struct { 16 21 Keys []crypto.JWK `json:"keys"` ··· 18 23 19 24 // Expected response type from looking up OAuth Protected Resource information on a server (eg, a PDS instance) 20 25 type ProtectedResourceMetadata struct { 21 - // TODO: are there other fields worth including? 26 + // are there other fields worth including? 22 27 23 28 AuthorizationServers []string `json:"authorization_servers"` 24 29 } ··· 73 78 PolicyURI *string `json:"policy_uri,omitempty"` 74 79 } 75 80 81 + // returns 'true' if client metadata indicates that this is a confidential client 82 + func (m *ClientMetadata) IsConfidential() bool { 83 + if (m.JWKSUri != nil || len(m.JWKS) > 0) && (m.TokenEndpointAuthMethod != nil && *m.TokenEndpointAuthMethod == "private_key_jwt") { 84 + return true 85 + } 86 + 87 + return false 88 + } 89 + 76 90 func (m *ClientMetadata) Validate(clientID string) error { 77 - // XXX: validate field syntax, possibly including consistency against a provided URL / clientID 78 - // XXX: copy from python tutorial 91 + 92 + if m.ClientID == "" || m.ClientID != clientID { 93 + return fmt.Errorf("%w: client_id", ErrInvalidClientMetadata) 94 + } 95 + 96 + if m.ApplicationType != nil && !slices.Contains([]string{"web", "native"}, *m.ApplicationType) { 97 + return fmt.Errorf("%w: application_type must be 'web', 'native', or undefined", ErrInvalidClientMetadata) 98 + } 99 + 100 + if !slices.Contains(m.GrantTypes, "authorization_code") { 101 + return fmt.Errorf("%w: grant_type must include 'authorization_code'", ErrInvalidClientMetadata) 102 + } 103 + 104 + scopes := strings.Split(m.Scope, " ") 105 + if !slices.Contains(scopes, "atproto") { 106 + return fmt.Errorf("%w: scope must include 'atproto'", ErrInvalidClientMetadata) 107 + } 108 + 109 + if !slices.Contains(m.ResponseTypes, "code") { 110 + return fmt.Errorf("%w: response_types must include 'code'", ErrInvalidClientMetadata) 111 + } 112 + 113 + if len(m.RedirectURIs) == 0 { 114 + return fmt.Errorf("%w: redirect_uris must have at least one element", ErrInvalidClientMetadata) 115 + } 116 + 117 + // 'web' redirect URLs have more restrictions 118 + if m.ApplicationType == nil || *m.ApplicationType == "web" { 119 + for _, ru := range m.RedirectURIs { 120 + u, err := url.Parse(ru) 121 + if err != nil { 122 + return fmt.Errorf("%w: invalid web redirect_uris: %w", ErrInvalidClientMetadata, err) 123 + } 124 + if u.Scheme != "https" { 125 + return fmt.Errorf("%w: web redirect_uris must have 'https' scheme", ErrInvalidClientMetadata) 126 + } 127 + } 128 + } 129 + 130 + if m.TokenEndpointAuthSigningAlg != nil && *m.TokenEndpointAuthSigningAlg == "none" { 131 + // NOTE: what if this is a public client? 132 + return fmt.Errorf("%w: token_endpoint_auth_signing_alg must not be 'none'", ErrInvalidClientMetadata) 133 + } 134 + 135 + if !m.DpopBoundAccessTokens { 136 + return fmt.Errorf("%w: dpop_bound_access_tokens must be true (DPoP is required)", ErrInvalidClientMetadata) 137 + } 138 + 139 + if m.JWKSUri != nil && *m.JWKSUri == "" { 140 + return fmt.Errorf("%w: jwks_uri must be valid URL (when provided)", ErrInvalidClientMetadata) 141 + } 142 + 143 + // NOTE: metadata URLs are not validated (they are not an error for overall metadata doc) 144 + 79 145 return nil 80 146 } 81 147 ··· 128 194 } 129 195 130 196 func (m *AuthServerMetadata) Validate(serverURL string) error { 131 - // XXX: check that Issuer matches domain this metadata document was fetched from 132 197 133 198 if m.Issuer == "" { 134 199 return fmt.Errorf("%w: empty issuer", ErrInvalidAuthServerMetadata) 135 200 } 136 201 u, err := url.Parse(m.Issuer) 137 202 if err != nil { 138 - return err 203 + return fmt.Errorf("%w: invalid issuer URL: %w", ErrInvalidAuthServerMetadata, err) 139 204 } 140 205 if u.Scheme != "https" || u.Port() != "" || u.Path != "" || u.Fragment != "" || u.RawQuery != "" { 141 206 return fmt.Errorf("%w: issuer URL", ErrInvalidAuthServerMetadata) 142 207 } 208 + 209 + // check that Issuer matches domain this metadata document was fetched from 210 + srvu, err := url.Parse(serverURL) 211 + if err != nil { 212 + return fmt.Errorf("%w: invalid request URL: %w", ErrInvalidAuthServerMetadata, err) 213 + } 214 + if u.Scheme != srvu.Scheme || u.Host != srvu.Host { 215 + return fmt.Errorf("%w: issuer must match request URL", ErrInvalidAuthServerMetadata) 216 + } 217 + 143 218 if !slices.Contains(m.ResponseTypesSupported, "code") { 144 219 return fmt.Errorf("%w: response_types_supported must include 'code'", ErrInvalidAuthServerMetadata) 145 220 } ··· 237 312 // If the flow started with an account identifier (DID or handle), it should be persisted, to verify against the initial token response. 238 313 AccountDID *syntax.DID `json:"account_did,omitempty"` 239 314 240 - // XXX: if we started from handle, should probably trust / use the resolved DID? 241 - //AccountHandle *syntax.Handle `json:"account_handle,omitempty"` 242 - 243 - // Base URL of the "resource server" (eg, PDS), if the auth flow started with a host URL instead of an account identifier. 244 - HostURL *string `json:"host_url,omitempty"` 245 - 246 315 // OAuth scope string (space-separated list) 247 316 Scope string `json:"scope"` 248 317 ··· 256 325 DpopAuthServerNonce string `json:"dpop_authserver_nonce"` 257 326 258 327 // The secret cryptographic key generated by the client for this specific OAuth session 259 - // TODO: better name for this field 260 - DpopKeyMultibase string `json:"dpop_privatekey_multibase"` 328 + DpopPrivateKeyMultibase string `json:"dpop_privatekey_multibase"` 261 329 } 262 330 263 331 // Persisted information about an OAuth session. Used to resume an active session. ··· 286 354 DpopHostNonce string `json:"dpop_host_nonce"` 287 355 288 356 // The secret cryptographic key generated by the client for this specific OAuth session 289 - DpopKeyMultibase string `json:"dpop_secret_key"` 357 + DpopPrivateKeyMultibase string `json:"dpop_privatekey_multibase"` 290 358 291 359 // TODO: also persist access token creation time / expiration time? In context that token might not be an easily parsed JWT 292 360 } ··· 296 364 // Client ID, aka client metadata URL 297 365 ClientID string `url:"client_id"` 298 366 299 - // Only used in initial token request. Client-specified URL that will get redirected to by auth server at end of user auth flow 300 - // XXX: is this really needed in the initial token request? 367 + // Only used in initial token request. Auth server will validate that this matches the redirect URI used during the auth flow (resulting in the auth code) 301 368 RedirectURI string `url:"redirect_uri"` 302 369 303 370 // Always `authorization_code` 304 371 GrantType string `url:"grant_type"` 305 372 306 - // Refresh token. 373 + // Refresh token 307 374 RefreshToken string `url:"refresh_token"` 308 375 376 + // Authorization Code provided by the Auth Server via callback at the end of the auth request flow 309 377 Code string `url:"code"` 310 378 311 - // PKCE verifier string. Only included in initial token request. 379 + // PKCE verifier string. Only included in initial token request 312 380 CodeVerifier string `url:"code_verifier"` 313 381 314 - // Always "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 315 - ClientAssertionType string `url:"client_assertion_type"` 382 + // For confidential clients, must be "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 383 + ClientAssertionType *string `url:"client_assertion_type"` 316 384 317 - // Confidential client signed JWT 318 - ClientAssertion string `url:"client_assertion"` 385 + // For confidential clients, the signed client assertion JWT 386 + ClientAssertion *string `url:"client_assertion"` 319 387 } 320 388 321 389 // The fields which are included in a token refresh request. These HTTP POST bodies are form-encoded, so use URL encoding syntax, not JSON. ··· 329 397 // Refresh token. 330 398 RefreshToken string `url:"refresh_token"` 331 399 332 - // Always "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 333 - ClientAssertionType string `url:"client_assertion_type"` 400 + // For confidential clients, must be "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 401 + ClientAssertionType *string `url:"client_assertion_type"` 334 402 335 - // Confidential client signed JWT 336 - ClientAssertion string `url:"client_assertion"` 403 + // For confidential clients, the signed client assertion JWT 404 + ClientAssertion *string `url:"client_assertion"` 337 405 } 338 406 339 407 // Expected respose from Auth Server token endpoint, both for initial token request and for refresh requests.