···3939test-jwks: ## Create a test jwks file
4040 go run ./cmd/cmd generate-jwks --prefix demo
41414242+.PHONY: jwks
4343+jwks:
4444+ go run ./cmd/cmd generate-jwks
4545+4246.env:
4347 if [ ! -f ".env" ]; then cp example.dev.env .env; fi
+230
README.md
···11+# Atproto OAuth Golang
22+33+> [!WARNING]
44+> This is an experimental repo. It may contain bugs. Use at your own risk.
55+66+> [!WARNING]
77+> 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.
88+99+1010+## Prerequisites
1111+There are some prerequisites that you'll need to handle before implementing this OAuth client.
1212+1313+### Private JWK
1414+If 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
1515+1616+`make jwks`
1717+1818+You will need to read the JWK from your application and parse it using `oauth.ParseJWKFromBytes`.
1919+2020+### Serve `client-metadata.json` from your application
2121+2222+The client metadata will need to be accessible from your domain. An example using `echo` is below.
2323+2424+```go
2525+func (s *TestServer) handleClientMetadata(e echo.Context) error {
2626+ metadata := map[string]any{
2727+ "client_id": serverMetadataUrl,
2828+ "client_name": "Atproto Oauth Golang Tester",
2929+ "client_uri": serverUrlRoot,
3030+ "logo_uri": fmt.Sprintf("%s/logo.png", serverUrlRoot),
3131+ "tos_uri": fmt.Sprintf("%s/tos", serverUrlRoot),
3232+ "policy_url": fmt.Sprintf("%s/policy", serverUrlRoot),
3333+ "redirect_uris": []string{serverCallbackUrl},
3434+ "grant_types": []string{"authorization_code", "refresh_token"},
3535+ "response_types": []string{"code"},
3636+ "application_type": "web",
3737+ "dpop_bound_access_tokens": true,
3838+ "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", serverUrlRoot),
3939+ "scope": "atproto transition:generic",
4040+ "token_endpoint_auth_method": "private_key_jwt",
4141+ "token_endpoint_auth_signing_alg": "ES256",
4242+ }
4343+4444+ return e.JSON(200, metadata)
4545+}
4646+```
4747+4848+### Serve `jwks.json`
4949+5050+You will also need to serve your private JWK's __public key__ from your domain. Again, an example is below.
5151+5252+```go
5353+func (s *TestServer) handleJwks(e echo.Context) error {
5454+ b, err := os.ReadFile("./jwk.json")
5555+ if err != nil {
5656+ return err
5757+ }
5858+5959+ k, err := oauth.ParseJWKFromBytes(b)
6060+ if err != nil {
6161+ return err
6262+ }
6363+6464+ pubKey, err := k.PublicKey()
6565+ if err != nil {
6666+ return err
6767+ }
6868+6969+ return e.JSON(200, oauth.CreateJwksResponseObject(pubKey))
7070+}
7171+```
7272+7373+## Usage
7474+7575+Once you have completed the prerequisites, you can implement and use the client.
7676+7777+### Create a new OAuth Client
7878+7979+Create an OAuth client by calling `oauth.NewClient`
8080+8181+```go
8282+clientId := "https://yourdomain.com/path/to/client-metadata.json"
8383+callbackUrl := "https://yourdomain.com/oauth-callback"
8484+8585+b, err := os.ReadFile("./jwks.json")
8686+if err != nil {
8787+ return err
8888+}
8989+9090+k, err := oauth.ParseJWKFromBytes(b)
9191+if err != nil {
9292+ return err
9393+}
9494+9595+cli, err := oauth.NewClient(oauth.ClientArgs{
9696+ ClientJwk: k,
9797+ ClientId: clientId,
9898+ RedirectUri: callbackUrl,
9999+})
100100+if err != nil {
101101+ return err
102102+}
103103+```
104104+105105+### Starting Authenticating
106106+107107+There are examples of the authentication flow inside of `cmd/client_tester/handle_auth.go`, however we'll talk about some general points here.
108108+109109+#### Determining the user's PDS
110110+111111+You 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.
112112+113113+```go
114114+cli := oauth.NewClient()
115115+userInput := "hailey.at"
116116+117117+// If you already have a did or a URL, you can skip this step
118118+did, err := resolveHandle(ctx, userInput) // returns did:plc:abc123 or did:web:test.com
119119+if err != nil {
120120+ return err
121121+}
122122+123123+// If you already have a URL, you can skip this step
124124+service, err := resolveService(ctx, did) // returns https://pds.haileyok.com
125125+if err != nil {
126126+ return err
127127+}
128128+129129+authserver, err := cli.ResolvePdsAuthServer(ctx, service)
130130+if err != nil {
131131+ return err
132132+}
133133+134134+authmeta, err := cli.FetchAuthServerMetadata(ctx, authserver)
135135+if err != nil {
136136+ return err
137137+}
138138+```
139139+140140+By this point, you will have the necessary information to direct the user where they need to go.
141141+142142+#### Create a private DPoP JWK for the user
143143+144144+You'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.
145145+146146+```go
147147+k, err := oauth.GenerateKey(nil)
148148+if err != nil {
149149+ return err
150150+}
151151+152152+b, err := json.Marshal(k)
153153+if err != nil {
154154+ return err
155155+}
156156+```
157157+158158+#### Make the PAR request
159159+160160+```go
161161+// Note: the login hint - here `handle` - should only be set if you have a DID or handle. Leave it empty if all you
162162+// have is the PDS url.
163163+parResp, err := cli.SendParAuthRequest(ctx, authserver, authmeta, handle, scope, dpopPrivateKey)
164164+if err != nil {
165165+ return err
166166+}
167167+```
168168+169169+#### Store the needed information before redirecting
170170+171171+Some items will need to be stored for later when the PDS redirects to your application.
172172+173173+- The user's DID, if you have it
174174+- The user's PDS url
175175+- The authserver issuer
176176+- The `state` value from the PAR request
177177+- The PKCE verifier from the PAR rquest
178178+- The DPoP autherserver nonce from the PAR request
179179+- The DPoP private JWK thhat you generated
180180+181181+It 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`.
182182+183183+#### Redirect
184184+185185+Once 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.
186186+187187+```go
188188+u, _ := url.Parse(meta.AuthorizationEndpoint)
189189+u.RawQuery = fmt.Sprintf("client_id=%s&requires_uri=%s", url.QueryEscape(yourClientId), parResp.RequestUri)
190190+191191+// Redirect the user to created url
192192+```
193193+194194+### Callback handling
195195+196196+Handling the response is pretty easy, though you'll want to check a few things once you receive the response.
197197+198198+- Ensure that `state`, `iss`, and `code` are present in the `GET` parameters
199199+- Ensure that the `state` value matches the `state` value you stored before redirection
200200+201201+You'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.
202202+203203+```go
204204+resCode := e.QueryParam("code")
205205+resIss := e.QueryParam("iss")
206206+207207+itResp, err := cli.InitialTokenRequest(ctx, resCode, resIss, requestInfo.PkceVerifier, requestInfo.DpopAuthserverNonce, requestInfo.privateJwk)
208208+if err != nil {
209209+ return err
210210+}
211211+```
212212+213213+#### Final checks
214214+215215+Finally, 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`.
216216+217217+```go
218218+if itResp.Scope != requestedScope {
219219+ return fmt.Errorf("bad scope")
220220+}
221221+222222+if requestInfo.Did == "" {
223223+ // Do something...
224224+}
225225+```
226226+227227+#### Store the response
228228+229229+Now, 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.
230230+