this repo has no description

add refreshing

+5 -4
cmd/client_test/handle_auth.go
···
"encoding/json"
"fmt"
"net/url"
+
"time"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/gorilla/sessions"
···
e.Request().Context(),
resCode,
resIss,
-
resIss,
oauthRequest.PkceVerifier,
oauthRequest.DpopAuthserverNonce,
jwk,
···
// TODO: resolve if needed
-
if initialTokenResp.Resp["scope"] != scope {
+
if initialTokenResp.Scope != scope {
return fmt.Errorf("did not receive correct scopes from token request")
}
···
Did: oauthRequest.Did,
PdsUrl: oauthRequest.PdsUrl,
AuthserverIss: oauthRequest.AuthserverIss,
-
AccessToken: initialTokenResp.Resp["access_token"].(string),
-
RefreshToken: initialTokenResp.Resp["refresh_token"].(string),
+
AccessToken: initialTokenResp.AccessToken,
+
RefreshToken: initialTokenResp.RefreshToken,
DpopAuthserverNonce: initialTokenResp.DpopAuthserverNonce,
DpopPrivateJwk: oauthRequest.DpopPrivateJwk,
+
Expiration: time.Now().Add(time.Duration(int(time.Second) * int(initialTokenResp.ExpiresIn))),
}
if err := s.db.Clauses(clause.OnConflict{
+4 -16
cmd/client_test/handle_post.go
···
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/bluesky-social/indigo/lex/util"
"github.com/bluesky-social/indigo/xrpc"
-
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
func (s *TestServer) handleMakePost(e echo.Context) error {
-
sess, err := session.Get("session", e)
+
authArgs, authed, err := s.getOauthSessionAuthArgs(e)
if err != nil {
return err
}
-
did, ok := sess.Values["did"]
-
if !ok {
+
if !authed {
return e.Redirect(302, "/login")
}
-
var oauthSession OauthSession
-
if err := s.db.Raw("SELECT * FROM oauth_sessions WHERE did = ?", did).Scan(&oauthSession).Error; err != nil {
-
return err
-
}
-
-
args, err := authedReqArgsFromSession(&oauthSession)
-
if err != nil {
-
return err
-
}
-
post := bsky.FeedPost{
Text: "hello from atproto golang oauth client",
CreatedAt: syntax.DatetimeNow().String(),
···
input := atproto.RepoCreateRecord_Input{
Collection: "app.bsky.feed.post",
-
Repo: oauthSession.Did,
+
Repo: authArgs.Did,
Record: &util.LexiconTypeDecoder{Val: &post},
}
var out atproto.RepoCreateRecord_Output
-
if err := s.xrpcCli.Do(e.Request().Context(), args, xrpc.Procedure, "application/json", "com.atproto.repo.createRecord", nil, input, &out); err != nil {
+
if err := s.xrpcCli.Do(e.Request().Context(), authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.createRecord", nil, input, &out); err != nil {
return err
}
+3 -15
cmd/client_test/handle_profile.go
···
import (
"github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/xrpc"
-
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
)
func (s *TestServer) handleProfile(e echo.Context) error {
-
sess, err := session.Get("session", e)
+
authArgs, authed, err := s.getOauthSessionAuthArgs(e)
if err != nil {
return err
}
-
did, ok := sess.Values["did"]
-
if !ok {
+
if !authed {
return e.Redirect(302, "/login")
}
-
var oauthSession OauthSession
-
if err := s.db.Raw("SELECT * FROM oauth_sessions WHERE did = ?", did).Scan(&oauthSession).Error; err != nil {
-
return err
-
}
-
-
args, err := authedReqArgsFromSession(&oauthSession)
-
if err != nil {
-
return err
-
}
-
var out bsky.ActorDefs_ProfileViewDetailed
-
if err := s.xrpcCli.Do(e.Request().Context(), args, xrpc.Query, "", "app.bsky.actor.getProfile", map[string]any{"actor": oauthSession.Did}, nil, &out); err != nil {
+
if err := s.xrpcCli.Do(e.Request().Context(), authArgs, xrpc.Query, "", "app.bsky.actor.getProfile", map[string]any{"actor": authArgs.Did}, nil, &out); err != nil {
return err
}
-16
cmd/client_test/main.go
···
return e.JSON(200, s.jwksResponse)
}
-
func authedReqArgsFromSession(session *OauthSession) (*oauth.XrpcAuthedRequestArgs, error) {
-
privateJwk, err := oauth.ParseKeyFromBytes([]byte(session.DpopPrivateJwk))
-
if err != nil {
-
return nil, err
-
}
-
-
return &oauth.XrpcAuthedRequestArgs{
-
Did: session.Did,
-
AccessToken: session.AccessToken,
-
PdsUrl: session.PdsUrl,
-
Issuer: session.AuthserverIss,
-
DpopPdsNonce: session.DpopPdsNonce,
-
DpopPrivateJwk: privateJwk,
-
}, nil
-
}
-
func getFilePath(file string) string {
return fmt.Sprintf("%s/%s", staticFilePath, file)
}
+6 -8
cmd/client_test/resolution.go
···
}
recs, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
-
if err != nil {
-
return "", err
-
}
-
-
for _, rec := range recs {
-
if strings.HasPrefix(rec, "did=") {
-
did = strings.Split(rec, "did=")[1]
-
break
+
if err == nil {
+
for _, rec := range recs {
+
if strings.HasPrefix(rec, "did=") {
+
did = strings.Split(rec, "did=")[1]
+
break
+
}
}
}
+3
cmd/client_test/types.go
···
package main
+
import "time"
+
type OauthRequest struct {
ID uint
AuthserverIss string
···
DpopPdsNonce string
DpopAuthserverNonce string
DpopPrivateJwk string
+
Expiration time.Time
}
+75
cmd/client_test/user.go
···
+
package main
+
+
import (
+
"context"
+
"fmt"
+
"time"
+
+
oauth "github.com/haileyok/atproto-oauth-golang"
+
"github.com/labstack/echo-contrib/session"
+
"github.com/labstack/echo/v4"
+
)
+
+
func (s *TestServer) getOauthSession(ctx context.Context, did string) (*OauthSession, error) {
+
var oauthSession OauthSession
+
if err := s.db.Raw("SELECT * FROM oauth_sessions WHERE did = ?", did).Scan(&oauthSession).Error; err != nil {
+
return nil, err
+
}
+
+
if oauthSession.Did == "" {
+
return nil, fmt.Errorf("did not find session in database")
+
}
+
+
if oauthSession.Expiration.Sub(time.Now()) <= 5*time.Minute {
+
privateJwk, err := oauth.ParseKeyFromBytes([]byte(oauthSession.DpopPrivateJwk))
+
if err != nil {
+
return nil, err
+
}
+
+
resp, err := s.oauthClient.RefreshTokenRequest(ctx, oauthSession.RefreshToken, oauthSession.AuthserverIss, oauthSession.DpopAuthserverNonce, privateJwk)
+
if err != nil {
+
return nil, err
+
}
+
+
expiration := time.Now().Add(time.Duration(int(time.Second) * int(resp.ExpiresIn)))
+
+
if err := s.db.Exec("UPDATE oauth_sessions SET access_token = ?, refresh_token = ?, dpop_authserver_nonce = ?, expiration = ? WHERE did = ?", resp.AccessToken, resp.RefreshToken, resp.DpopAuthserverNonce, expiration, oauthSession.Did).Error; err != nil {
+
return nil, err
+
}
+
+
oauthSession.AccessToken = resp.AccessToken
+
oauthSession.RefreshToken = resp.RefreshToken
+
oauthSession.DpopAuthserverNonce = resp.DpopAuthserverNonce
+
oauthSession.Expiration = expiration
+
}
+
+
return &oauthSession, nil
+
}
+
+
func (s *TestServer) getOauthSessionAuthArgs(e echo.Context) (*oauth.XrpcAuthedRequestArgs, bool, error) {
+
sess, err := session.Get("session", e)
+
if err != nil {
+
return nil, false, err
+
}
+
+
did, ok := sess.Values["did"]
+
if !ok {
+
return nil, false, nil
+
}
+
+
oauthSession, err := s.getOauthSession(e.Request().Context(), did.(string))
+
+
privateJwk, err := oauth.ParseKeyFromBytes([]byte(oauthSession.DpopPrivateJwk))
+
if err != nil {
+
return nil, false, err
+
}
+
+
return &oauth.XrpcAuthedRequestArgs{
+
Did: oauthSession.Did,
+
AccessToken: oauthSession.AccessToken,
+
PdsUrl: oauthSession.PdsUrl,
+
Issuer: oauthSession.AuthserverIss,
+
DpopPdsNonce: oauthSession.DpopPdsNonce,
+
DpopPrivateJwk: privateJwk,
+
}, true, nil
+
}
+68 -72
oauth.go
···
func (c *Client) InitialTokenRequest(
ctx context.Context,
code,
-
appUrl,
authserverIss,
pkceVerifier,
dpopAuthserverNonce string,
···
"client_assertion": {clientAssertion},
}
-
dpopProof, err := c.AuthServerDpopJwt(
-
"POST",
-
authserverMeta.TokenEndpoint,
-
dpopAuthserverNonce,
-
dpopPrivateJwk,
-
)
+
dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, dpopAuthserverNonce, dpopPrivateJwk)
if err != nil {
return nil, err
}
-
req, err := http.NewRequestWithContext(
-
ctx,
-
"POST",
-
authserverMeta.TokenEndpoint,
-
strings.NewReader(params.Encode()),
-
)
+
req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
if err != nil {
return nil, err
}
···
}
defer resp.Body.Close()
-
var rmap map[string]any
-
if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
+
var tokenResponse TokenResponse
+
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
return nil, err
}
-
return &TokenResponse{
-
DpopAuthserverNonce: dpopAuthserverNonce,
-
Resp: rmap,
-
}, nil
+
tokenResponse.DpopAuthserverNonce = dpopAuthserverNonce
+
+
return &tokenResponse, nil
}
-
func (c *Client) RefreshTokenRequest(ctx context.Context, authserverUrl, refreshToken, dpopAuthserverNonce string, dpopPrivateJwk jwk.Key) (*TokenResponse, error) {
-
authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverUrl)
-
if err != nil {
-
return nil, err
-
}
+
func (c *Client) RefreshTokenRequest(
+
ctx context.Context,
+
refreshToken,
+
authserverIss,
+
dpopAuthserverNonce string,
+
dpopPrivateJwk jwk.Key,
+
) (*TokenResponse, error) {
+
// we may need to update the dpop nonce
+
for range 2 {
+
authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss)
+
if err != nil {
+
return nil, err
+
}
-
clientAssertion, err := c.ClientAssertionJwt(authserverUrl)
-
if err != nil {
-
return nil, err
-
}
+
clientAssertion, err := c.ClientAssertionJwt(authserverIss)
+
if err != nil {
+
return nil, err
+
}
-
params := url.Values{
-
"client_id": {c.clientId},
-
"grant_type": {"refresh_token"},
-
"refresh_token": {refreshToken},
-
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
-
"client_assertion": {clientAssertion},
-
}
+
params := url.Values{
+
"client_id": {c.clientId},
+
"grant_type": {"refresh_token"},
+
"refresh_token": {refreshToken},
+
"client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
+
"client_assertion": {clientAssertion},
+
}
-
dpopProof, err := c.AuthServerDpopJwt(
-
"POST",
-
authserverMeta.TokenEndpoint,
-
dpopAuthserverNonce,
-
dpopPrivateJwk,
-
)
-
if err != nil {
-
return nil, err
-
}
+
dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, dpopAuthserverNonce, dpopPrivateJwk)
+
if err != nil {
+
return nil, err
+
}
-
req, err := http.NewRequestWithContext(
-
ctx,
-
"POST",
-
authserverMeta.TokenEndpoint,
-
strings.NewReader(params.Encode()),
-
)
-
if err != nil {
-
return nil, err
-
}
+
req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
+
if err != nil {
+
return nil, err
+
}
-
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
req.Header.Set("DPoP", dpopProof)
+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
req.Header.Set("DPoP", dpopProof)
-
resp, err := c.h.Do(req)
-
if err != nil {
-
return nil, err
-
}
-
defer resp.Body.Close()
+
resp, err := c.h.Do(req)
+
if err != nil {
+
return nil, err
+
}
+
defer resp.Body.Close()
-
// TODO: handle same thing as above...
+
if resp.StatusCode != 200 && resp.StatusCode != 201 {
+
var respMap map[string]string
+
if err := json.NewDecoder(resp.Body).Decode(&respMap); err != nil {
+
return nil, err
+
}
+
+
if resp.StatusCode == 400 && respMap["error"] == "use_dpop_nonce" {
+
dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce")
+
continue
+
}
+
+
return nil, fmt.Errorf("token refresh error: %s", respMap["error"])
+
}
+
+
var tokenResponse TokenResponse
+
if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
+
return nil, err
+
}
-
if resp.StatusCode != 200 && resp.StatusCode != 201 {
-
b, _ := io.ReadAll(resp.Body)
-
return nil, fmt.Errorf("token refresh error: %s", string(b))
-
}
+
// set the nonce so that updates are reflected in response
+
tokenResponse.DpopAuthserverNonce = dpopAuthserverNonce
-
var rmap map[string]any
-
if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
-
return nil, err
+
return &tokenResponse, nil
}
-
return &TokenResponse{
-
DpopAuthserverNonce: dpopAuthserverNonce,
-
Resp: rmap,
-
}, nil
+
return nil, nil
}
func generateToken(len int) (string, error) {
+7 -2
types.go
···
)
type TokenResponse struct {
-
DpopAuthserverNonce string
-
Resp map[string]any
+
DpopAuthserverNonce string `json:"-"`
+
AccessToken string `json:"access_token"`
+
RefreshToken string `json:"refresh_token"`
+
ExpiresIn int `json:"expires_in"`
+
Scope string `json:"scope"`
+
Sub string `json:"sub"`
+
TokenType string `json:"token_type"`
}
type RefreshTokenArgs struct {
+3 -4
xrpc.go
···
// if we get a new nonce, update the nonce and make the request again
if (resp.StatusCode == 400 || resp.StatusCode == 401) && xe.ErrStr == "use_dpop_nonce" {
-
newNonce := resp.Header.Get("DPoP-Nonce")
-
c.OnDPoPNonceChanged(authedArgs.Did, newNonce)
-
authedArgs.DpopPdsNonce = newNonce
+
authedArgs.DpopPdsNonce = resp.Header.Get("DPoP-Nonce")
+
c.OnDPoPNonceChanged(authedArgs.Did, authedArgs.DpopPdsNonce)
continue
}
···
}
}
-
break
+
return nil
}
return nil