atproto_auth 0.0.1 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +17 -2
- data/CHANGELOG.md +23 -2
- data/README.md +91 -5
- data/examples/confidential_client/.gitignore +2 -0
- data/examples/confidential_client/Gemfile +1 -0
- data/examples/confidential_client/Gemfile.lock +10 -1
- data/examples/confidential_client/README.md +86 -9
- data/examples/confidential_client/app.rb +83 -12
- data/examples/confidential_client/{public/client-metadata.json → config/client-metadata.example.json} +5 -4
- data/examples/confidential_client/screenshots/screenshot-1-sign-in.png +0 -0
- data/examples/confidential_client/screenshots/screenshot-2-success.png +0 -0
- data/examples/confidential_client/scripts/generate_keys.rb +0 -0
- data/examples/confidential_client/views/authorized.erb +1 -1
- data/lib/atproto_auth/client.rb +98 -38
- data/lib/atproto_auth/client_metadata.rb +2 -2
- data/lib/atproto_auth/configuration.rb +35 -1
- data/lib/atproto_auth/dpop/key_manager.rb +1 -1
- data/lib/atproto_auth/dpop/nonce_manager.rb +30 -47
- data/lib/atproto_auth/encryption.rb +156 -0
- data/lib/atproto_auth/http_client.rb +2 -2
- data/lib/atproto_auth/identity/document.rb +1 -1
- data/lib/atproto_auth/identity/resolver.rb +1 -1
- data/lib/atproto_auth/serialization/base.rb +189 -0
- data/lib/atproto_auth/serialization/dpop_key.rb +29 -0
- data/lib/atproto_auth/serialization/session.rb +77 -0
- data/lib/atproto_auth/serialization/stored_nonce.rb +37 -0
- data/lib/atproto_auth/serialization/token_set.rb +43 -0
- data/lib/atproto_auth/server_metadata/authorization_server.rb +20 -1
- data/lib/atproto_auth/state/session_manager.rb +67 -20
- data/lib/atproto_auth/storage/interface.rb +112 -0
- data/lib/atproto_auth/storage/key_builder.rb +39 -0
- data/lib/atproto_auth/storage/memory.rb +191 -0
- data/lib/atproto_auth/storage/redis.rb +119 -0
- data/lib/atproto_auth/token/refresh.rb +249 -0
- data/lib/atproto_auth/version.rb +1 -1
- data/lib/atproto_auth.rb +29 -1
- metadata +32 -5
- data/examples/confidential_client/config/client-metadata.json +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '08926445ef54c28c9bc2a53020b757fc00b713ee3dc727904d70716c0838990c'
|
4
|
+
data.tar.gz: 01e90659b36f0ae44ea4b23333677ff15ba0da41532d429405a70c4bf6102055
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae789a761c830082d70a21307165914c413b5a5b64043eeaf321ffeefe359b7c83f6f3cdf12264c0cab2fae9f709654cecc01b849ca022b87a97d82f7cbb9855
|
7
|
+
data.tar.gz: c69972fe73bb865280ec9a3f631827f34b9116bbc8e2e00c8fb7dcec61037ff6f40969778273f3d82031ee13dd3e98327a2580ebd1f95c84e4533312ecfb9076
|
data/.rubocop.yml
CHANGED
@@ -3,11 +3,26 @@ AllCops:
|
|
3
3
|
SuggestExtensions: false
|
4
4
|
TargetRubyVersion: 3.0
|
5
5
|
|
6
|
+
Metrics/BlockLength:
|
7
|
+
Enabled: false
|
8
|
+
|
6
9
|
Metrics/ClassLength:
|
7
|
-
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Metrics/ModuleLength:
|
13
|
+
Enabled: false
|
8
14
|
|
9
15
|
Metrics/MethodLength:
|
10
|
-
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Metrics/AbcSize:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Metrics/PerceivedComplexity:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Metrics/CyclomaticComplexity:
|
25
|
+
Enabled: false
|
11
26
|
|
12
27
|
Style/StringLiterals:
|
13
28
|
EnforcedStyle: double_quotes
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
1
|
-
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
2
3
|
|
3
|
-
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [0.1.0] - 2024-12-07
|
8
|
+
|
9
|
+
### Added
|
10
|
+
- Configurable storage backend system for managing OAuth state
|
11
|
+
- In-memory storage implementation included by default
|
12
|
+
- Redis storage implementation for production environments
|
13
|
+
- Thread-safe storage operations with atomic locks
|
14
|
+
- Storage encryption for sensitive data
|
15
|
+
- Automatic cleanup of expired tokens and session data
|
16
|
+
- Storage interface for custom backend implementations
|
17
|
+
|
18
|
+
### Changed
|
19
|
+
- Storage configuration is now required in AtprotoAuth.configure
|
20
|
+
- Default configuration uses thread-safe in-memory storage
|
21
|
+
- Session and token management now use configured storage backend
|
22
|
+
- Improved thread safety for all storage operations
|
23
|
+
|
24
|
+
## [0.0.1] - 2024-12-05
|
4
25
|
|
5
26
|
- Initial release
|
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# AtprotoAuth
|
2
2
|
|
3
|
-
[![Gem Version](https://
|
4
|
-
[![Ruby Style Guide](https://img.shields.io/badge/code_style-
|
3
|
+
[![Gem Version](https://img.shields.io/gem/v/atproto_auth.svg)](https://rubygems.org/gems/atproto_auth)
|
4
|
+
[![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-blue.svg)](https://github.com/rubocop/rubocop)
|
5
5
|
[![Documentation](https://img.shields.io/badge/docs-rdoc-blue.svg)](https://www.rubydoc.info/gems/atproto_auth)
|
6
6
|
|
7
|
-
A Ruby implementation of the AT Protocol OAuth specification. This library provides
|
7
|
+
A Ruby implementation of the [AT Protocol OAuth specification](https://docs.bsky.app/docs/advanced-guides/oauth-client). This library provides support for both client and server-side implementations, with built-in security features including [DPoP](https://datatracker.ietf.org/doc/html/rfc9449), [PAR](https://datatracker.ietf.org/doc/html/rfc9126), and dynamic client registration.
|
8
8
|
|
9
9
|
## Features
|
10
10
|
|
@@ -15,6 +15,8 @@ A Ruby implementation of the AT Protocol OAuth specification. This library provi
|
|
15
15
|
- Comprehensive identity resolution and verification
|
16
16
|
- Automatic token refresh and session management
|
17
17
|
- Robust error handling and recovery mechanisms
|
18
|
+
- Configurable storage backends with built-in Redis support
|
19
|
+
- Encrypted storage of sensitive data
|
18
20
|
|
19
21
|
## Installation
|
20
22
|
|
@@ -38,9 +40,10 @@ gem install atproto_auth
|
|
38
40
|
|
39
41
|
## Requirements
|
40
42
|
|
41
|
-
- Ruby 3.
|
43
|
+
- Ruby 3.3 or higher
|
42
44
|
- OpenSSL support
|
43
45
|
- For confidential clients: HTTPS-capable domain for client metadata hosting
|
46
|
+
- Optional: Redis 5.0+ for production storage backend
|
44
47
|
|
45
48
|
## Basic Usage
|
46
49
|
|
@@ -59,6 +62,88 @@ AtprotoAuth.configure do |config|
|
|
59
62
|
# Set token lifetimes
|
60
63
|
config.default_token_lifetime = 300 # 5 minutes
|
61
64
|
config.dpop_nonce_lifetime = 300 # 5 minutes
|
65
|
+
|
66
|
+
# Configure storage backend (default is in-memory)
|
67
|
+
config.storage = AtprotoAuth::Storage::Memory.new
|
68
|
+
end
|
69
|
+
|
70
|
+
# For production environments, use Redis storage:
|
71
|
+
AtprotoAuth.configure do |config|
|
72
|
+
# Configure Redis storage
|
73
|
+
config.storage = AtprotoAuth::Storage::Redis.new(
|
74
|
+
redis_client: Redis.new(url: ENV['REDIS_URL'])
|
75
|
+
)
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
### Storage Backends
|
80
|
+
|
81
|
+
The library supports multiple storage backends for managing OAuth state:
|
82
|
+
|
83
|
+
#### In-Memory Storage (Default)
|
84
|
+
```ruby
|
85
|
+
# Default configuration - good for development
|
86
|
+
AtprotoAuth.configure do |config|
|
87
|
+
config.storage = AtprotoAuth::Storage::Memory.new
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
#### Redis Storage (Recommended for Production)
|
92
|
+
```ruby
|
93
|
+
# Redis configuration - recommended for production
|
94
|
+
require 'redis'
|
95
|
+
|
96
|
+
AtprotoAuth.configure do |config|
|
97
|
+
redis_client = Redis.new(
|
98
|
+
url: ENV['REDIS_URL'],
|
99
|
+
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_PEER }
|
100
|
+
)
|
101
|
+
|
102
|
+
config.storage = AtprotoAuth::Storage::Redis.new(
|
103
|
+
redis_client: redis_client
|
104
|
+
)
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
#### Custom Storage Implementation
|
109
|
+
```ruby
|
110
|
+
# Implement your own storage backend
|
111
|
+
class CustomStorage < AtprotoAuth::Storage::Interface
|
112
|
+
def set(key, value, ttl: nil)
|
113
|
+
# Implementation
|
114
|
+
end
|
115
|
+
|
116
|
+
def get(key)
|
117
|
+
# Implementation
|
118
|
+
end
|
119
|
+
|
120
|
+
def delete(key)
|
121
|
+
# Implementation
|
122
|
+
end
|
123
|
+
|
124
|
+
def exists?(key)
|
125
|
+
# Implementation
|
126
|
+
end
|
127
|
+
|
128
|
+
def multi_get(keys)
|
129
|
+
# Implementation
|
130
|
+
end
|
131
|
+
|
132
|
+
def multi_set(hash, ttl: nil)
|
133
|
+
# Implementation
|
134
|
+
end
|
135
|
+
|
136
|
+
def acquire_lock(key, ttl:)
|
137
|
+
# Implementation
|
138
|
+
end
|
139
|
+
|
140
|
+
def release_lock(key)
|
141
|
+
# Implementation
|
142
|
+
end
|
143
|
+
|
144
|
+
def with_lock(key, ttl: 30)
|
145
|
+
# Implementation
|
146
|
+
end
|
62
147
|
end
|
63
148
|
```
|
64
149
|
|
@@ -162,7 +247,8 @@ Built-in security best practices:
|
|
162
247
|
- Constant-time token comparisons
|
163
248
|
- Thread-safe state management
|
164
249
|
- Protection against SSRF attacks
|
165
|
-
- Secure token storage
|
250
|
+
- Secure encrypted token storage
|
251
|
+
- Atomic storage operations with locking
|
166
252
|
|
167
253
|
## Development
|
168
254
|
|
@@ -1,15 +1,17 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ../..
|
3
3
|
specs:
|
4
|
-
atproto_auth (0.1.
|
4
|
+
atproto_auth (0.1.1)
|
5
5
|
jose (~> 1.2)
|
6
6
|
jwt (~> 2.9)
|
7
|
+
redis (~> 5.3)
|
7
8
|
|
8
9
|
GEM
|
9
10
|
remote: https://rubygems.org/
|
10
11
|
specs:
|
11
12
|
base64 (0.2.0)
|
12
13
|
concurrent-ruby (1.3.4)
|
14
|
+
connection_pool (2.4.1)
|
13
15
|
dotenv (3.1.4)
|
14
16
|
faraday (2.12.1)
|
15
17
|
faraday-net_http (>= 2.0, < 3.5)
|
@@ -45,6 +47,10 @@ GEM
|
|
45
47
|
rackup (2.2.1)
|
46
48
|
rack (>= 3)
|
47
49
|
rbtree (0.4.6)
|
50
|
+
redis (5.3.0)
|
51
|
+
redis-client (>= 0.22.0)
|
52
|
+
redis-client (0.22.2)
|
53
|
+
connection_pool
|
48
54
|
ruby2_keywords (0.0.5)
|
49
55
|
set (1.1.1)
|
50
56
|
sinatra (4.1.1)
|
@@ -80,5 +86,8 @@ DEPENDENCIES
|
|
80
86
|
sinatra
|
81
87
|
sinatra-contrib
|
82
88
|
|
89
|
+
RUBY VERSION
|
90
|
+
ruby 3.3.5p100
|
91
|
+
|
83
92
|
BUNDLED WITH
|
84
93
|
2.5.23
|
@@ -2,14 +2,19 @@
|
|
2
2
|
|
3
3
|
This is an example implementation of a confidential OAuth client for the AT Protocol using the AtprotoAuth gem. It demonstrates how to implement the OAuth flow for a web application, including DPoP token binding and secure session management.
|
4
4
|
|
5
|
+
<img src="https://github.com/jhuckabee/atproto_auth/blob/main/examples/confidential_client/screenshots/screenshot-1-sign-in.png?raw=true" alt="Sign In Form Screenshot" title="Sign In Form" width="500">
|
6
|
+
|
7
|
+
<img src="https://github.com/jhuckabee/atproto_auth/blob/main/examples/confidential_client/screenshots/screenshot-2-success.png?raw=true" alt="Sign In Success Screenshot" title="Sign In Success" width="500">
|
8
|
+
|
5
9
|
## Overview
|
6
10
|
|
7
11
|
The example implements a simple web application using Sinatra that:
|
8
12
|
- Allows users to sign in with their AT Protocol handle (@handle)
|
9
13
|
- Implements the complete OAuth authorization flow
|
10
14
|
- Uses DPoP-bound tokens for API requests
|
11
|
-
- Demonstrates secure session management
|
15
|
+
- Demonstrates secure session management with encryption
|
12
16
|
- Shows how to make authenticated API calls to Bluesky
|
17
|
+
- Provides examples of both development and production storage configurations
|
13
18
|
|
14
19
|
## Requirements
|
15
20
|
|
@@ -17,6 +22,7 @@ The example implements a simple web application using Sinatra that:
|
|
17
22
|
- Bundler
|
18
23
|
- A domain name for your application that matches your client metadata
|
19
24
|
- SSL certificate for your domain (required for production)
|
25
|
+
- Redis (optional, recommended for production)
|
20
26
|
|
21
27
|
## Setup
|
22
28
|
|
@@ -35,19 +41,61 @@ bundle install
|
|
35
41
|
bundle exec ruby scripts/generate_keys.rb > config/keys.json
|
36
42
|
```
|
37
43
|
|
38
|
-
4. Configure your client metadata
|
44
|
+
4. Configure your client metadata:
|
45
|
+
- Copy the example metadata file over:
|
46
|
+
```
|
47
|
+
cp config/client-metadata.example.json config/client-metadata.json
|
48
|
+
```
|
39
49
|
- Set the correct `client_id` URL where your metadata will be hosted
|
40
50
|
- Configure valid `redirect_uris` for your application
|
41
51
|
- Add your generated keys from step 3 to the `jwks` field
|
42
52
|
|
43
53
|
5. Set up environment variables:
|
44
54
|
```bash
|
45
|
-
|
46
|
-
export
|
55
|
+
# Required for session encryption
|
56
|
+
export SESSION_SECRET=your-secure-session-secret
|
57
|
+
|
58
|
+
# Your application's domain name
|
59
|
+
export PERMITTED_DOMAIN=your.domain.com
|
60
|
+
|
61
|
+
# Optional: Redis URL for production storage
|
62
|
+
export REDIS_URL=redis://localhost:6379
|
47
63
|
```
|
48
64
|
|
49
65
|
## Configuration
|
50
66
|
|
67
|
+
### Storage Configuration
|
68
|
+
|
69
|
+
The example app supports both in-memory and Redis storage backends:
|
70
|
+
|
71
|
+
#### Development (In-Memory Storage)
|
72
|
+
```ruby
|
73
|
+
# config/development.rb
|
74
|
+
AtprotoAuth.configure do |config|
|
75
|
+
config.storage = AtprotoAuth::Storage::Memory.new
|
76
|
+
config.logger = Logger.new($stdout)
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
#### Production (Redis Storage)
|
81
|
+
```ruby
|
82
|
+
# config/production.rb
|
83
|
+
require 'redis'
|
84
|
+
|
85
|
+
AtprotoAuth.configure do |config|
|
86
|
+
redis_client = Redis.new(
|
87
|
+
url: ENV.fetch('REDIS_URL'),
|
88
|
+
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_PEER }
|
89
|
+
)
|
90
|
+
|
91
|
+
config.storage = AtprotoAuth::Storage::Redis.new(
|
92
|
+
redis_client: redis_client
|
93
|
+
)
|
94
|
+
|
95
|
+
config.logger = Logger.new($stdout)
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
51
99
|
### Host Authorization
|
52
100
|
|
53
101
|
This application requires specific domain configuration to function properly:
|
@@ -69,7 +117,7 @@ This application requires specific domain configuration to function properly:
|
|
69
117
|
```bash
|
70
118
|
export PERMITTED_DOMAIN=machinename.xyz.ts.net
|
71
119
|
```
|
72
|
-
6. Run the application (see below)
|
120
|
+
6. Run the application (see below)
|
73
121
|
|
74
122
|
Your application will now be accessible via your Tailscale domain with HTTPS enabled.
|
75
123
|
|
@@ -77,20 +125,25 @@ Your application will now be accessible via your Tailscale domain with HTTPS ena
|
|
77
125
|
|
78
126
|
The application uses encrypted sessions to store authorization data. Configure the session secret with:
|
79
127
|
|
80
|
-
```
|
128
|
+
```bash
|
81
129
|
export SESSION_SECRET=your-secure-random-string
|
82
130
|
```
|
83
131
|
|
84
|
-
If not set, a random secret will be generated on startup.
|
132
|
+
If not set, a random secret will be generated on startup (not recommended for production).
|
85
133
|
|
86
134
|
## Running the Application
|
87
135
|
|
136
|
+
### Development
|
88
137
|
```bash
|
89
|
-
bundle exec rackup
|
138
|
+
RACK_ENV=development bundle exec rackup
|
90
139
|
```
|
91
140
|
|
92
|
-
|
141
|
+
### Production
|
142
|
+
```bash
|
143
|
+
RACK_ENV=production bundle exec rackup -E production
|
144
|
+
```
|
93
145
|
|
146
|
+
This will start the server on `http://localhost:9292`.
|
94
147
|
|
95
148
|
## Troubleshooting
|
96
149
|
|
@@ -108,3 +161,27 @@ This will start the server on `http://localhost:9292`.
|
|
108
161
|
- Verify your JWKS configuration
|
109
162
|
- Check that your DPoP proofs are being generated correctly
|
110
163
|
- Ensure your client authentication is working
|
164
|
+
|
165
|
+
4. "Storage errors":
|
166
|
+
- For Redis storage, verify Redis connection settings
|
167
|
+
- Check Redis SSL configuration if using encrypted connections
|
168
|
+
- Ensure proper Redis authentication credentials if required
|
169
|
+
|
170
|
+
5. "Session state lost":
|
171
|
+
- Verify storage configuration is correct
|
172
|
+
- Check Redis connection stability if using Redis storage
|
173
|
+
- Ensure session TTLs are appropriately configured
|
174
|
+
|
175
|
+
## Understanding the Code
|
176
|
+
|
177
|
+
The example demonstrates several important concepts:
|
178
|
+
|
179
|
+
1. **Secure Storage**: The app shows proper configuration of both development (in-memory) and production (Redis) storage backends.
|
180
|
+
|
181
|
+
2. **Token Management**: All tokens are stored securely with encryption in the configured storage backend.
|
182
|
+
|
183
|
+
3. **Session Handling**: The app demonstrates proper session state management with atomic operations and locking.
|
184
|
+
|
185
|
+
4. **Error Recovery**: Includes examples of handling storage failures and token refresh scenarios.
|
186
|
+
|
187
|
+
The code is thoroughly commented to explain these concepts and their implementation details.
|
@@ -9,17 +9,18 @@ require "dotenv/load"
|
|
9
9
|
|
10
10
|
# Main app entry point
|
11
11
|
class ExampleApp < Sinatra::Base
|
12
|
+
def check_stored_session(session_id)
|
13
|
+
return false unless session_id
|
14
|
+
|
15
|
+
settings.oauth_client.authorized?(session_id)
|
16
|
+
rescue AtprotoAuth::SessionError
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
12
20
|
configure :development do
|
13
21
|
register Sinatra::Reloader
|
14
22
|
end
|
15
23
|
|
16
|
-
set :host_authorization, {
|
17
|
-
permitted_hosts: ["localhost", ENV.fetch("PERMITTED_DOMAIN", nil)].compact
|
18
|
-
}
|
19
|
-
|
20
|
-
enable :sessions
|
21
|
-
set :session_secret, ENV.fetch("SESSION_SECRET") { SecureRandom.hex(32) }
|
22
|
-
|
23
24
|
# Initialize the AT Protocol OAuth client
|
24
25
|
configure do
|
25
26
|
# Configure AtprotoAuth settings
|
@@ -30,6 +31,9 @@ class ExampleApp < Sinatra::Base
|
|
30
31
|
)
|
31
32
|
config.default_token_lifetime = 300
|
32
33
|
config.dpop_nonce_lifetime = 300
|
34
|
+
|
35
|
+
# Optionally, use Redis storage instead of in-memory
|
36
|
+
# config.storage = AtprotoAuth::Storage::Redis.new
|
33
37
|
end
|
34
38
|
|
35
39
|
# Load client metadata
|
@@ -45,10 +49,61 @@ class ExampleApp < Sinatra::Base
|
|
45
49
|
)
|
46
50
|
end
|
47
51
|
|
52
|
+
set :host_authorization, {
|
53
|
+
permitted_hosts: ["localhost", ENV.fetch("PERMITTED_DOMAIN", nil)].compact
|
54
|
+
}
|
55
|
+
|
56
|
+
use Rack::Session::Cookie,
|
57
|
+
key: "atproto.session",
|
58
|
+
expire_after: 86_400, # 1 day in seconds
|
59
|
+
secret: ENV.fetch("SESSION_SECRET") { SecureRandom.hex(32) },
|
60
|
+
secure: true, # Only send over HTTPS
|
61
|
+
httponly: true, # Not accessible via JavaScript
|
62
|
+
same_site: :lax # CSRF protection
|
63
|
+
|
64
|
+
helpers do
|
65
|
+
def recover_session
|
66
|
+
session_id = session[:oauth_session_id]
|
67
|
+
return nil unless session_id
|
68
|
+
|
69
|
+
begin
|
70
|
+
# Check if session is still valid
|
71
|
+
return nil unless settings.oauth_client.authorized?(session_id)
|
72
|
+
|
73
|
+
session_id
|
74
|
+
rescue AtprotoAuth::Client::SessionError
|
75
|
+
# Clear invalid session
|
76
|
+
session.delete(:oauth_session_id)
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
48
82
|
get "/" do
|
83
|
+
# Check for existing session
|
84
|
+
redirect "/authorized" if recover_session
|
85
|
+
|
49
86
|
erb :index
|
50
87
|
end
|
51
88
|
|
89
|
+
get "/client-metadata.json" do
|
90
|
+
content_type :json
|
91
|
+
|
92
|
+
# Read metadata from config file
|
93
|
+
metadata_path = File.join(__dir__, "config", "client-metadata.json")
|
94
|
+
metadata = JSON.parse(File.read(metadata_path))
|
95
|
+
|
96
|
+
# Strip private key 'd' component from each key in the JWKS
|
97
|
+
if metadata["jwks"] && metadata["jwks"]["keys"]
|
98
|
+
metadata["jwks"]["keys"] = metadata["jwks"]["keys"].map do |key|
|
99
|
+
key.except("d")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Return sanitized metadata
|
104
|
+
JSON.generate(metadata)
|
105
|
+
end
|
106
|
+
|
52
107
|
# Start OAuth flow
|
53
108
|
post "/auth" do
|
54
109
|
handle = params[:handle]
|
@@ -60,7 +115,7 @@ class ExampleApp < Sinatra::Base
|
|
60
115
|
scope: "atproto"
|
61
116
|
)
|
62
117
|
|
63
|
-
# Store session ID
|
118
|
+
# Store session ID in user's browser session
|
64
119
|
session[:oauth_session_id] = auth[:session_id]
|
65
120
|
|
66
121
|
# Redirect to authorization URL
|
@@ -80,8 +135,8 @@ class ExampleApp < Sinatra::Base
|
|
80
135
|
iss: params[:iss]
|
81
136
|
)
|
82
137
|
|
83
|
-
# Store tokens
|
84
|
-
session[:
|
138
|
+
# Store tokens
|
139
|
+
session[:oauth_session_id] = result[:session_id]
|
85
140
|
|
86
141
|
redirect "/authorized"
|
87
142
|
rescue StandardError => e
|
@@ -91,9 +146,19 @@ class ExampleApp < Sinatra::Base
|
|
91
146
|
|
92
147
|
# Show authorized state and test API call
|
93
148
|
get "/authorized" do
|
94
|
-
|
149
|
+
session_id = session[:oauth_session_id]
|
150
|
+
return redirect "/" unless check_stored_session(session_id)
|
95
151
|
|
96
152
|
begin
|
153
|
+
# Get current session tokens
|
154
|
+
oauth_session = settings.oauth_client.get_tokens(session_id)
|
155
|
+
|
156
|
+
# Check if token needs refresh
|
157
|
+
if oauth_session[:expires_in] < 30
|
158
|
+
# Refresh token
|
159
|
+
oauth_session = settings.oauth_client.refresh_token(session_id)
|
160
|
+
end
|
161
|
+
|
97
162
|
# Make test API call to com.atproto.identity.resolveHandle
|
98
163
|
conn = Faraday.new(url: "https://api.bsky.app") do |f|
|
99
164
|
f.request :json
|
@@ -102,7 +167,7 @@ class ExampleApp < Sinatra::Base
|
|
102
167
|
|
103
168
|
# Get auth headers for request
|
104
169
|
headers = settings.oauth_client.auth_headers(
|
105
|
-
session_id:
|
170
|
+
session_id: session_id,
|
106
171
|
method: "GET",
|
107
172
|
url: "https://api.bsky.app/xrpc/com.atproto.identity.resolveHandle"
|
108
173
|
)
|
@@ -114,6 +179,7 @@ class ExampleApp < Sinatra::Base
|
|
114
179
|
end
|
115
180
|
|
116
181
|
@api_result = response.body
|
182
|
+
@session = oauth_session
|
117
183
|
erb :authorized
|
118
184
|
rescue StandardError => e
|
119
185
|
session[:error] = "API call failed: #{e.message}"
|
@@ -122,6 +188,11 @@ class ExampleApp < Sinatra::Base
|
|
122
188
|
end
|
123
189
|
|
124
190
|
get "/signout" do
|
191
|
+
if session[:oauth_session_id]
|
192
|
+
# Clean up stored session
|
193
|
+
settings.oauth_client.remove_session(session[:oauth_session_id])
|
194
|
+
end
|
195
|
+
|
125
196
|
session.clear
|
126
197
|
session[:notice] = "Successfully signed out"
|
127
198
|
redirect "/"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
|
-
"client_id": "https://
|
2
|
+
"client_id": "https://YOUR_DOMAIN_HERE/client-metadata.json",
|
3
3
|
"client_name": "AT Protocol OAuth Ruby Example",
|
4
|
-
"redirect_uris": ["https://
|
4
|
+
"redirect_uris": ["https://YOUR_DOMAIN_HERE/callback"],
|
5
5
|
"grant_types": ["authorization_code", "refresh_token"],
|
6
6
|
"response_types": ["code"],
|
7
7
|
"scope": "atproto",
|
@@ -14,10 +14,11 @@
|
|
14
14
|
{
|
15
15
|
"use": "sig",
|
16
16
|
"kid": "key-1",
|
17
|
-
"x": "
|
17
|
+
"x": "...",
|
18
18
|
"crv": "P-256",
|
19
|
+
"d": "...",
|
19
20
|
"kty": "EC",
|
20
|
-
"y": "
|
21
|
+
"y": "..."
|
21
22
|
}
|
22
23
|
]
|
23
24
|
}
|
Binary file
|
Binary file
|
File without changes
|
@@ -13,7 +13,7 @@
|
|
13
13
|
|
14
14
|
<div class="token-info">
|
15
15
|
<p class="mb-1 text-gray-600 dark:text-gray-400 text-sm font-medium">Token Information</p>
|
16
|
-
<pre class="rounded-lg text-gray-700 dark:text-gray-100 bg-gray-100 dark:bg-slate-800 border border-gray-400 p-2 overflow-auto"><code><%= JSON.pretty_generate(session
|
16
|
+
<pre class="rounded-lg text-gray-700 dark:text-gray-100 bg-gray-100 dark:bg-slate-800 border border-gray-400 p-2 overflow-auto"><code><%= JSON.pretty_generate(@session) %></code></pre>
|
17
17
|
</div>
|
18
18
|
|
19
19
|
<div class="api-test">
|