Live video on the AT Protocol
at eli/docker-linting 156 lines 6.5 kB view raw
1package oproxy 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "time" 8 9 oauth "github.com/haileyok/atproto-oauth-golang" 10 "github.com/lestrrat-go/jwx/v2/jwk" 11) 12 13var refreshWhenRemaining = time.Minute * 15 14 15// OAuthSession stores authentication data needed during the OAuth flow 16type OAuthSession struct { 17 DID string `json:"did" gorm:"column:repo_did;index"` 18 Handle string `json:"handle" gorm:"column:handle;index"` // possibly also did if they have no handle 19 PDSUrl string `json:"pds_url" gorm:"column:pds_url;index"` 20 21 // Upstream fields 22 UpstreamState string `json:"upstream_state" gorm:"column:upstream_state;index"` 23 UpstreamAuthServerIssuer string `json:"upstream_auth_server_issuer" gorm:"column:upstream_auth_server_issuer"` 24 UpstreamPKCEVerifier string `json:"upstream_pkce_verifier" gorm:"column:upstream_pkce_verifier"` 25 UpstreamDPoPNonce string `json:"upstream_dpop_nonce" gorm:"column:upstream_dpop_nonce"` 26 UpstreamDPoPPrivateJWK string `json:"upstream_dpop_private_jwk" gorm:"column:upstream_dpop_private_jwk;type:text"` 27 UpstreamAccessToken string `json:"upstream_access_token" gorm:"column:upstream_access_token"` 28 UpstreamAccessTokenExp *time.Time `json:"upstream_access_token_exp" gorm:"column:upstream_access_token_exp"` 29 UpstreamRefreshToken string `json:"upstream_refresh_token" gorm:"column:upstream_refresh_token"` 30 31 // Downstream fields 32 DownstreamDPoPNonce string `json:"downstream_dpop_nonce" gorm:"column:downstream_dpop_nonce"` 33 DownstreamDPoPJKT string `json:"downstream_dpop_jkt" gorm:"column:downstream_dpop_jkt;primaryKey"` 34 DownstreamAccessToken string `json:"downstream_access_token" gorm:"column:downstream_access_token;index"` 35 DownstreamRefreshToken string `json:"downstream_refresh_token" gorm:"column:downstream_refresh_token;index"` 36 DownstreamAuthorizationCode string `json:"downstream_authorization_code" gorm:"column:downstream_authorization_code;index"` 37 DownstreamState string `json:"downstream_state" gorm:"column:downstream_state"` 38 DownstreamScope string `json:"downstream_scope" gorm:"column:downstream_scope"` 39 DownstreamCodeChallenge string `json:"downstream_code_challenge" gorm:"column:downstream_code_challenge"` 40 DownstreamPARRequestURI string `json:"downstream_par_request_uri" gorm:"column:downstream_par_request_uri"` 41 DownstreamPARUsedAt *time.Time `json:"downstream_par_used_at" gorm:"column:downstream_par_used_at"` 42 DownstreamRedirectURI string `json:"downstream_redirect_uri" gorm:"column:downstream_redirect_uri"` 43 44 RevokedAt *time.Time `json:"revoked_at" gorm:"column:revoked_at"` 45 CreatedAt time.Time `json:"created_at"` 46 UpdatedAt time.Time `json:"updated_at"` 47} 48 49// for gorm. this is prettier than "o_auth_sessions" 50func (o *OAuthSession) TableName() string { 51 return "oauth_sessions" 52} 53 54type OAuthSessionStatus string 55 56const ( 57 // We've gotten the first request and sent it back for a new nonce 58 OAuthSessionStatePARPending OAuthSessionStatus = "par-pending" 59 // PAR has been created, but not yet used 60 OAuthSessionStatePARCreated OAuthSessionStatus = "par-created" 61 // PAR has been used, but maybe upstream will fail for some reason 62 OAuthSessionStatePARUsed OAuthSessionStatus = "par-used" 63 // PAR has been used, we're waiting to hear back from upstream 64 OAuthSessionStateUpstream OAuthSessionStatus = "upstream" 65 // Upstream came back, we've issued the user a code but it hasn't been used yet 66 OAuthSessionStateDownstream OAuthSessionStatus = "downstream" 67 // Code has been used, everything is good 68 OAuthSessionStateReady OAuthSessionStatus = "ready" 69 // For any reason we're done. Revoked or expired 70 OAuthSessionStateRejected OAuthSessionStatus = "rejected" 71) 72 73func (o *OAuthSession) Status() OAuthSessionStatus { 74 if o.RevokedAt != nil { 75 return OAuthSessionStateRejected 76 } 77 if o.DownstreamAccessToken != "" { 78 return OAuthSessionStateReady 79 } 80 if o.DownstreamAuthorizationCode != "" { 81 return OAuthSessionStateDownstream 82 } 83 if o.UpstreamDPoPPrivateJWK != "" { 84 return OAuthSessionStateUpstream 85 } 86 if o.DownstreamPARUsedAt != nil { 87 return OAuthSessionStatePARUsed 88 } 89 if o.DownstreamPARRequestURI != "" { 90 return OAuthSessionStatePARCreated 91 } 92 if o.DownstreamDPoPNonce != "" { 93 return OAuthSessionStatePARPending 94 } 95 bs, _ := json.Marshal(o) 96 fmt.Printf("unknown oauth session status: %s\n", string(bs)) 97 // todo: this should never happen, log a warning? panic? 98 return OAuthSessionStateRejected 99} 100 101func (o *OProxy) loadOAuthSession(jkt string) (*OAuthSession, error) { 102 session, err := o.userLoadOAuthSession(jkt) 103 if err != nil { 104 return nil, err 105 } 106 if session == nil { 107 return nil, nil 108 } 109 if session.Status() != OAuthSessionStateReady { 110 return session, nil 111 } 112 if session.UpstreamAccessTokenExp.Sub(time.Now()) > refreshWhenRemaining { 113 return session, nil 114 } 115 116 upstreamMeta := o.GetUpstreamMetadata() 117 118 oclient, err := oauth.NewClient(oauth.ClientArgs{ 119 ClientJwk: o.upstreamJWK, 120 ClientId: upstreamMeta.ClientID, 121 RedirectUri: upstreamMeta.RedirectURIs[0], 122 }) 123 124 dpopKey, err := jwk.ParseKey([]byte(session.UpstreamDPoPPrivateJWK)) 125 if err != nil { 126 return nil, fmt.Errorf("failed to parse upstream dpop private key: %w", err) 127 } 128 129 // refresh upstream before returning 130 resp, err := oclient.RefreshTokenRequest(context.Background(), session.UpstreamRefreshToken, session.UpstreamAuthServerIssuer, session.UpstreamDPoPNonce, dpopKey) 131 if err != nil { 132 // revoke, probably 133 o.slog.Error("failed to refresh upstream token, revoking downstream session", "error", err) 134 now := time.Now() 135 session.RevokedAt = &now 136 err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 137 if err != nil { 138 o.slog.Error("after upstream token refresh, failed to revoke downstream session", "error", err) 139 } 140 return nil, fmt.Errorf("failed to refresh upstream token: %w", err) 141 } 142 143 exp := time.Now().Add(time.Second * time.Duration(resp.ExpiresIn)).UTC() 144 session.UpstreamAccessToken = resp.AccessToken 145 session.UpstreamAccessTokenExp = &exp 146 session.UpstreamRefreshToken = resp.RefreshToken 147 148 err = o.updateOAuthSession(session.DownstreamDPoPJKT, session) 149 if err != nil { 150 return nil, fmt.Errorf("failed to update downstream session after upstream token refresh: %w", err) 151 } 152 153 o.slog.Debug("refreshed upstream token", "session", session.DownstreamDPoPJKT) 154 155 return session, nil 156}