1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package oauthclient
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strings"
"sync"
"github.com/bluesky-social/indigo/atproto/atcrypto"
)
var (
ErrNoSession = errors.New("oauthclient: no session stored")
ErrSessionExpired = errors.New("oauthclient: the oauth session expired")
ErrInvalidSession = errors.New("oauthclient: invalid stored oauth session")
ErrNoRefreshToken = errors.New("oauthclient: the oauth session has no refresh token")
ErrQRConsumed = errors.New("oauthclient: the qr session transfer was already consumed")
ErrQRTimeout = errors.New("oauthclient: the qr session transfer timed out")
ErrStateNotFound = errors.New("oauthclient: the oauth callback state is unknown")
ErrScopeRejected = errors.New("oauthclient: the granted scope does not include atproto; the session is rejected")
)
type SessionData struct {
AccountDID string `json:"account_did"`
SessionID string `json:"session_id"`
HostURL string `json:"host_url"`
ClientID string `json:"client_id,omitempty"`
AuthServerURL string `json:"authserver_url"`
AuthServerTokenEndpoint string `json:"authserver_token_endpoint"`
AuthServerRevocationEndpoint string `json:"authserver_revocation_endpoint,omitempty"`
Scopes []string `json:"scopes"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
DPoPAuthServerNonce string `json:"dpop_authserver_nonce"`
DPoPHostNonce string `json:"dpop_host_nonce"`
DPoPPrivateKeyMultibase string `json:"dpop_privatekey_multibase"`
}
type SessionStore interface {
List(ctx context.Context) ([]SessionData, error)
Save(ctx context.Context, session SessionData) error
Delete(ctx context.Context, did, sessionID string) error
}
type MemoryStore struct {
mu sync.Mutex
sessions map[string]SessionData
}
func (m *MemoryStore) List(ctx context.Context) ([]SessionData, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
m.mu.Lock()
defer m.mu.Unlock()
out := make([]SessionData, 0, len(m.sessions))
for _, session := range m.sessions {
out = append(out, session)
}
return out, nil
}
func (m *MemoryStore) Save(ctx context.Context, session SessionData) error {
if err := ctx.Err(); err != nil {
return err
}
if session.AccountDID == "" {
return errors.New("oauthclient: the oauth session names no account DID")
}
m.mu.Lock()
defer m.mu.Unlock()
if m.sessions == nil {
m.sessions = map[string]SessionData{}
}
m.sessions[SessionKey(session.AccountDID, session.SessionID)] = session
return nil
}
func (m *MemoryStore) Delete(ctx context.Context, did, sessionID string) error {
if err := ctx.Err(); err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
delete(m.sessions, SessionKey(did, sessionID))
return nil
}
func privateKeyFromJWK(kty, d string) (atcrypto.PrivateKeyExportable, error) {
if d == "" {
return nil, errors.New("oauthclient: the oauth session has no DPoP private key")
}
if kty != "" && kty != "EC" {
return nil, fmt.Errorf("oauthclient: the oauth session DPoP key type %q is not EC", kty)
}
keyBytes, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(d, "="))
if err != nil {
return nil, fmt.Errorf("oauthclient: the oauth session DPoP key: %w", err)
}
key, err := atcrypto.ParsePrivateBytesP256(keyBytes)
if err != nil {
return nil, fmt.Errorf("oauthclient: the oauth session DPoP key: %w", err)
}
return key, nil
}
func SessionKey(did, sessionID string) string {
return did + "/" + sessionID
}
func SessionID(did, host string) string {
sum := sha256.Sum256([]byte(did + "\x00" + host))
return hex.EncodeToString(sum[:16])
}