this repo has no description
1# Atproto OAuth Golang 2 3> [!WARNING] 4> This is an experimental repo. It may contain bugs. Use at your own risk. 5 6> [!WARNING] 7> You should always validate user input. The example/test code inside this repo may be used as an implementation guide, but no guarantees are made. 8 9```bash 10go get github.com/haileyok/atproto-oauth-golang 11``` 12 13## Prerequisites 14There are some prerequisites that you'll need to handle before implementing this OAuth client. 15 16### Private JWK 17If you do not already have a private JWK for your application, first create one. There is a helper CLI tool that can generate one for you. From the project directory, run 18 19`make jwks` 20 21You will need to read the JWK from your application and parse it using `oauth.ParseJWKFromBytes`. 22 23### Serve `client-metadata.json` from your application 24 25The client metadata will need to be accessible from your domain. An example using `echo` is below. 26 27```go 28func (s *TestServer) handleClientMetadata(e echo.Context) error { 29 metadata := map[string]any{ 30 "client_id": serverMetadataUrl, 31 "client_name": "Atproto Oauth Golang Tester", 32 "client_uri": serverUrlRoot, 33 "logo_uri": fmt.Sprintf("%s/logo.png", serverUrlRoot), 34 "tos_uri": fmt.Sprintf("%s/tos", serverUrlRoot), 35 "policy_url": fmt.Sprintf("%s/policy", serverUrlRoot), 36 "redirect_uris": []string{serverCallbackUrl}, 37 "grant_types": []string{"authorization_code", "refresh_token"}, 38 "response_types": []string{"code"}, 39 "application_type": "web", 40 "dpop_bound_access_tokens": true, 41 "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", serverUrlRoot), 42 "scope": "atproto transition:generic", 43 "token_endpoint_auth_method": "private_key_jwt", 44 "token_endpoint_auth_signing_alg": "ES256", 45 } 46 47 return e.JSON(200, metadata) 48} 49``` 50 51### Serve `jwks.json` 52 53You will also need to serve your private JWK's __public key__ from your domain. Again, an example is below. 54 55```go 56func (s *TestServer) handleJwks(e echo.Context) error { 57 b, err := os.ReadFile("./jwk.json") 58 if err != nil { 59 return err 60 } 61 62 k, err := oauth.ParseJWKFromBytes(b) 63 if err != nil { 64 return err 65 } 66 67 pubKey, err := k.PublicKey() 68 if err != nil { 69 return err 70 } 71 72 return e.JSON(200, oauth.CreateJwksResponseObject(pubKey)) 73} 74``` 75 76## Usage 77 78Once you have completed the prerequisites, you can implement and use the client. 79 80### Create a new OAuth Client 81 82Create an OAuth client by calling `oauth.NewClient` 83 84```go 85clientId := "https://yourdomain.com/path/to/client-metadata.json" 86callbackUrl := "https://yourdomain.com/oauth-callback" 87 88b, err := os.ReadFile("./jwks.json") 89if err != nil { 90 return err 91} 92 93k, err := oauth.ParseJWKFromBytes(b) 94if err != nil { 95 return err 96} 97 98cli, err := oauth.NewClient(oauth.ClientArgs{ 99 ClientJwk: k, 100 ClientId: clientId, 101 RedirectUri: callbackUrl, 102}) 103if err != nil { 104 return err 105} 106``` 107 108### Starting Authenticating 109 110There are examples of the authentication flow inside of `cmd/client_tester/handle_auth.go`, however we'll talk about some general points here. 111 112#### Determining the user's PDS 113 114You should allow for users to input their handle, DID, or PDS URL when detemrining where to send the user for authentication. An example that covers all the bases of what you'll need to do is when a user uses their handle. 115 116```go 117cli := oauth.NewClient() 118userInput := "hailey.at" 119 120// If you already have a did or a URL, you can skip this step 121did, err := resolveHandle(ctx, userInput) // returns did:plc:abc123 or did:web:test.com 122if err != nil { 123 return err 124} 125 126// If you already have a URL, you can skip this step 127service, err := resolveService(ctx, did) // returns https://pds.haileyok.com 128if err != nil { 129 return err 130} 131 132authserver, err := cli.ResolvePdsAuthServer(ctx, service) 133if err != nil { 134 return err 135} 136 137authmeta, err := cli.FetchAuthServerMetadata(ctx, authserver) 138if err != nil { 139 return err 140} 141``` 142 143By this point, you will have the necessary information to direct the user where they need to go. 144 145#### Create a private DPoP JWK for the user 146 147You'll need to create a private DPoP JWK for the user before directing them to their PDS to authenticate. You'll need to store this in a later step, and you will need to pass it along inside the PAR request, so go ahead and marshal it as well. 148 149```go 150k, err := oauth.GenerateKey(nil) 151if err != nil { 152 return err 153} 154 155b, err := json.Marshal(k) 156if err != nil { 157 return err 158} 159``` 160 161#### Make the PAR request 162 163```go 164// Note: the login hint - here `handle` - should only be set if you have a DID or handle. Leave it empty if all you 165// have is the PDS url. 166parResp, err := cli.SendParAuthRequest(ctx, authserver, authmeta, handle, scope, dpopPrivateKey) 167if err != nil { 168 return err 169} 170``` 171 172#### Store the needed information before redirecting 173 174Some items will need to be stored for later when the PDS redirects to your application. 175 176- The user's DID, if you have it 177- The user's PDS url 178- The authserver issuer 179- The `state` value from the PAR request 180- The PKCE verifier from the PAR rquest 181- The DPoP autherserver nonce from the PAR request 182- The DPoP private JWK thhat you generated 183 184It is up to you how you want to store these values. Most likely, you will want to store them in a database. You may also want to store the `state` variable in the user's session _as well as the database_ so you can verify it later. There's a basic implementation inside of `cmd/client_tester/handle_auth.go`. 185 186#### Redirect 187 188Once you've stored the needed info, send the user to their PDS. The URL to redirect the user to should have both the `client_id` and `request_uri` `GET` parameters set. 189 190```go 191u, _ := url.Parse(meta.AuthorizationEndpoint) 192u.RawQuery = fmt.Sprintf("client_id=%s&requires_uri=%s", url.QueryEscape(yourClientId), parResp.RequestUri) 193 194// Redirect the user to created url 195``` 196 197### Callback handling 198 199Handling the response is pretty easy, though you'll want to check a few things once you receive the response. 200 201- Ensure that `state`, `iss`, and `code` are present in the `GET` parameters 202- Ensure that the `state` value matches the `state` value you stored before redirection 203 204You'll next need to load all of the request information you previously stored. Once you have that information, you can perform the initial token request. 205 206```go 207resCode := e.QueryParam("code") 208resIss := e.QueryParam("iss") 209 210itResp, err := cli.InitialTokenRequest(ctx, resCode, resIss, requestInfo.PkceVerifier, requestInfo.DpopAuthserverNonce, requestInfo.privateJwk) 211if err != nil { 212 return err 213} 214``` 215 216#### Final checks 217 218Finally, check that the scope received matches the requested scope. Also, if you didn't have the user's DID before redirecting earlier, you can now get their DID from `itResp.Sub`. 219 220```go 221if itResp.Scope != requestedScope { 222 return fmt.Errorf("bad scope") 223} 224 225if requestInfo.Did == "" { 226 // Do something... 227} 228``` 229 230#### Store the response 231 232Now, you can store the response items to make make authenticated requests later. You likely will want to store at least the user's DID in a secure session so that you know who the user is. 233