···17171818Create a single [ClientApp] instance during service setup that will be used (concurrently) across all users and sessions:
19192020-```
2121-oauthScope := "atproto transition:generic"
2222-config := oauth.NewPublicConfig(
2323- "https://app.example.com/client-metadata.json",
2424- "https://app.example.com/oauth/callback",
2525-)
2020+ config := oauth.NewPublicConfig(
2121+ "https://app.example.com/client-metadata.json",
2222+ "https://app.example.com/oauth/callback",
2323+ []string{"transition:generic"},
2424+ )
26252727-// clients are "public" by default, but if they have secure access to a secret attestation key can be "confidential"
2828-2929-if CLIENT_SECRET_KEY != "" {
3030- priv, err := crypto.ParsePrivateMultibase(CLIENT_SECRET_KEY)
3131- if err != nil {
3232- return err
2626+ // clients are "public" by default, but if they have secure access to a secret attestation key can be "confidential"
2727+ if CLIENT_SECRET_KEY != "" {
2828+ priv, err := crypto.ParsePrivateMultibase(CLIENT_SECRET_KEY)
2929+ if err != nil {
3030+ return err
3131+ }
3232+ if err := config.AddClientSecret(priv, "example1"); err != nil {
3333+ return err
3434+ }
3335 }
3434- if err := config.AddClientSecret(priv, "example1"); err != nil {
3535- return err
3636- }
3737-}
38363939-oauthApp := oauth.NewClientApp(&config, oauth.NewMemStore())
4040-```
3737+ oauthApp := oauth.NewClientApp(&config, oauth.NewMemStore())
41384239For a real service, you would want to use a database or other peristant storage instead of [MemStore]. Otherwise all user sessions are dropped every time the process restarts.
43404441The client metadata document needs to be served at the URL indicated by the `client_id`. This can be done statically, or dynamically generated and served from the configuation:
45424646-```
4747-http.HandleFunc("GET /client-metadata.json", HandleClientMetadata)
4343+ http.HandleFunc("GET /client-metadata.json", HandleClientMetadata)
48444949-func HandleClientMetadata(w http.ResponseWriter, r *http.Request) {
5050- doc := oauthApp.Config.ClientMetadata(oauthScope)
5151- w.Header().Set("Content-Type", "application/json")
5252- if err := json.NewEncoder(w).Encode(doc); err != nil {
5353- http.Error(w, err.Error(), http.StatusInternalServerError)
5454- return
4545+ func HandleClientMetadata(w http.ResponseWriter, r *http.Request) {
4646+ doc := oauthApp.Config.ClientMetadata()
4747+ w.Header().Set("Content-Type", "application/json")
4848+ if err := json.NewEncoder(w).Encode(doc); err != nil {
4949+ http.Error(w, err.Error(), http.StatusInternalServerError)
5050+ return
5151+ }
5552 }
5656-}
5757-```
58535959-The login auth flow starts with a user identifier, which could be an atproto handle, DID, or a host URL. The high-level [StartAuthFlow()] method will resolve the identifier, send an auth request (PAR) to the server, persist request metadata in the [OAuthStore], and return a redirect URL for the user to visit:
5454+The login auth flow starts with a user identifier, which could be an atproto handle, DID, or an auth server URL (eg, a PDS). The high-level [StartAuthFlow()] method will resolve the identifier, send an auth request (PAR) to the server, persist request metadata in the [OAuthStore], and return a redirect URL for the user to visit:
60556161-```
6262-http.HandleFunc("GET /oauth/login", HandleLogin)
5656+ http.HandleFunc("GET /oauth/login", HandleLogin)
63576464-func HandleLogin(w http.ResponseWriter, r *http.Request) {
6565- ctx := r.Context()
5858+ func HandleLogin(w http.ResponseWriter, r *http.Request) {
5959+ ctx := r.Context()
66606767- // parse login identifier from the request
6868- identifier := "..."
6161+ // parse login identifier from the request
6262+ identifier := "..."
69637070- redirectURL, err := oauthApp.StartAuthFlow(ctx, identifier)
7171- if err != nil {
7272- http.Error(w, err.Error(), http.StatusInternalServerError)
6464+ redirectURL, err := oauthApp.StartAuthFlow(ctx, identifier)
6565+ if err != nil {
6666+ http.Error(w, err.Error(), http.StatusInternalServerError)
6767+ }
6868+ http.Redirect(w, r, redirectURL, http.StatusFound)
7369 }
7474- http.Redirect(w, r, redirectURL, http.StatusFound)
7575-}
7676-```
77707871The service then waits for a callback request on the configured endpoint. The [ProcessCallback()] method will load the earlier request metadata from the [OAuthStore], send an initial token request to the auth server, and validate that the session is consistent with the identifier from the begining of the login flow.
79728080-```
8181-http.HandleFunc("GET /client-metadata.json", HandleClientMetadata)
7373+ http.HandleFunc("GET /oauth/callback", HandleOAuthCallback)
82748383-func HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
8484- ctx := r.Context()
7575+ func HandleOAuthCallback(w http.ResponseWriter, r *http.Request) {
7676+ ctx := r.Context()
85778686- sessData, err := oauthApp.ProcessCallback(ctx, r.URL.Query())
8787- if err != nil {
8888- http.Error(w, err.Error(), http.StatusInternalServerError)
8989- }
7878+ sessData, err := oauthApp.ProcessCallback(ctx, r.URL.Query())
7979+ if err != nil {
8080+ http.Error(w, err.Error(), http.StatusInternalServerError)
8181+ }
90829191- // web services might record the DID in a secure session cookie
9292- _ = sessData.AccountDID
8383+ // web services might record the DID in a secure session cookie
8484+ _ = sessData.AccountDID
93859494- http.Redirect(w, r, "/app", http.StatusFound)
9595-}
9696-```
8686+ http.Redirect(w, r, "/app", http.StatusFound)
8787+ }
97889889Finally, sessions can be resumed and used to make authenticated API calls to the user's host:
9990100100-```
101101-// web services might use a secure session cookie to determine user's DID for a request
102102-did := syntax.DID("did:plc:abc123")
9191+ // web services might use a secure session cookie to determine user's DID for a request
9292+ did := syntax.DID("did:plc:abc123")
10393104104-sess, err := oauthApp.ResumeSession(ctx, did)
105105-if err != nil {
106106- return err
107107-}
9494+ sess, err := oauthApp.ResumeSession(ctx, did)
9595+ if err != nil {
9696+ return err
9797+ }
10898109109-c := sess.APIClient()
9999+ c := sess.APIClient()
110100111111-body := map[string]any{
112112- "repo": *c.AccountDID,
113113- "collection": "app.bsky.feed.post",
114114- "record": map[string]any{
115115- "$type": "app.bsky.feed.post",
116116- "text": "Hello World via OAuth!",
117117- "createdAt": syntax.DatetimeNow(),
118118- },
119119-}
101101+ body := map[string]any{
102102+ "repo": *c.AccountDID,
103103+ "collection": "app.bsky.feed.post",
104104+ "record": map[string]any{
105105+ "$type": "app.bsky.feed.post",
106106+ "text": "Hello World via OAuth!",
107107+ "createdAt": syntax.DatetimeNow(),
108108+ },
109109+ }
120110121121-if err := c.Post(ctx, "com.atproto.repo.createRecord", body, nil); err != nil {
122122- return err
123123-}
124124-```
111111+ if err := c.Post(ctx, "com.atproto.repo.createRecord", body, nil); err != nil {
112112+ return err
113113+ }
125114126115The [ClientSession] will handle nonce updates and token refreshes, and persist the results in the [OAuthStore].
127116