atproto_auth 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +16 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +179 -0
- data/Rakefile +16 -0
- data/examples/confidential_client/Gemfile +12 -0
- data/examples/confidential_client/Gemfile.lock +84 -0
- data/examples/confidential_client/README.md +110 -0
- data/examples/confidential_client/app.rb +136 -0
- data/examples/confidential_client/config/client-metadata.json +25 -0
- data/examples/confidential_client/config.ru +4 -0
- data/examples/confidential_client/public/client-metadata.json +24 -0
- data/examples/confidential_client/public/styles.css +70 -0
- data/examples/confidential_client/scripts/generate_keys.rb +15 -0
- data/examples/confidential_client/views/authorized.erb +29 -0
- data/examples/confidential_client/views/index.erb +44 -0
- data/examples/confidential_client/views/layout.erb +11 -0
- data/lib/atproto_auth/client.rb +410 -0
- data/lib/atproto_auth/client_metadata.rb +264 -0
- data/lib/atproto_auth/configuration.rb +17 -0
- data/lib/atproto_auth/dpop/client.rb +122 -0
- data/lib/atproto_auth/dpop/key_manager.rb +235 -0
- data/lib/atproto_auth/dpop/nonce_manager.rb +138 -0
- data/lib/atproto_auth/dpop/proof_generator.rb +112 -0
- data/lib/atproto_auth/errors.rb +47 -0
- data/lib/atproto_auth/http_client.rb +227 -0
- data/lib/atproto_auth/identity/document.rb +104 -0
- data/lib/atproto_auth/identity/resolver.rb +221 -0
- data/lib/atproto_auth/identity.rb +24 -0
- data/lib/atproto_auth/par/client.rb +203 -0
- data/lib/atproto_auth/par/client_assertion.rb +50 -0
- data/lib/atproto_auth/par/request.rb +140 -0
- data/lib/atproto_auth/par/response.rb +23 -0
- data/lib/atproto_auth/par.rb +40 -0
- data/lib/atproto_auth/pkce.rb +105 -0
- data/lib/atproto_auth/server_metadata/authorization_server.rb +175 -0
- data/lib/atproto_auth/server_metadata/origin_url.rb +51 -0
- data/lib/atproto_auth/server_metadata/resource_server.rb +71 -0
- data/lib/atproto_auth/server_metadata.rb +24 -0
- data/lib/atproto_auth/state/session.rb +117 -0
- data/lib/atproto_auth/state/session_manager.rb +75 -0
- data/lib/atproto_auth/state/token_set.rb +68 -0
- data/lib/atproto_auth/state.rb +54 -0
- data/lib/atproto_auth/version.rb +5 -0
- data/lib/atproto_auth.rb +56 -0
- data/sig/atproto_auth/client_metadata.rbs +95 -0
- data/sig/atproto_auth/dpop/client.rbs +38 -0
- data/sig/atproto_auth/dpop/key_manager.rbs +33 -0
- data/sig/atproto_auth/dpop/nonce_manager.rbs +48 -0
- data/sig/atproto_auth/dpop/proof_generator.rbs +42 -0
- data/sig/atproto_auth/http_client.rbs +58 -0
- data/sig/atproto_auth/identity/document.rbs +31 -0
- data/sig/atproto_auth/identity/resolver.rbs +41 -0
- data/sig/atproto_auth/par/client.rbs +31 -0
- data/sig/atproto_auth/par/request.rbs +73 -0
- data/sig/atproto_auth/par/response.rbs +17 -0
- data/sig/atproto_auth/pkce.rbs +24 -0
- data/sig/atproto_auth/server_metadata/authorization_server.rbs +69 -0
- data/sig/atproto_auth/server_metadata/origin_url.rbs +21 -0
- data/sig/atproto_auth/server_metadata/resource_server.rbs +27 -0
- data/sig/atproto_auth/state/session.rbs +50 -0
- data/sig/atproto_auth/state/session_manager.rbs +26 -0
- data/sig/atproto_auth/state/token_set.rbs +40 -0
- data/sig/atproto_auth/version.rbs +3 -0
- data/sig/atproto_auth.rbs +39 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 32471644f76751fd94dd3e7ede93e8b88c43494a23afa6b5d48196163d2d427c
|
4
|
+
data.tar.gz: 3a92957c782c800c23be621be8ecf85da19c4f3b2145705d79d3b8a9ac898d73
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b19ee50bae30ffba5a45deea718a7587c2db0c02a0f9ead117d2750e87943473ba8758c72fe5f8246d1499cd67bd2996d38f6144396130fd55252fb0e3acf5f8
|
7
|
+
data.tar.gz: a0f12a5e81da053323fd2712f21626bb6e719b45282a26039c480d37bce652998d0180b96c47751c4245792cbdaed8a8f02798790042b6eb50ea9e14feb158b6
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: enable
|
3
|
+
SuggestExtensions: false
|
4
|
+
TargetRubyVersion: 3.0
|
5
|
+
|
6
|
+
Metrics/ClassLength:
|
7
|
+
Max: 500
|
8
|
+
|
9
|
+
Metrics/MethodLength:
|
10
|
+
Max: 50
|
11
|
+
|
12
|
+
Style/StringLiterals:
|
13
|
+
EnforcedStyle: double_quotes
|
14
|
+
|
15
|
+
Style/StringLiteralsInInterpolation:
|
16
|
+
EnforcedStyle: double_quotes
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Josh Huckabee
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
# AtprotoAuth
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/atproto_auth)
|
4
|
+
[](https://github.com/testdouble/standard)
|
5
|
+
[](https://www.rubydoc.info/gems/atproto_auth)
|
6
|
+
|
7
|
+
A Ruby implementation of the AT Protocol OAuth specification. This library provides comprehensive support for both client and server-side implementations, with built-in security features including DPoP (Demonstrating Proof of Possession), PAR (Pushed Authorization Requests), and dynamic client registration.
|
8
|
+
|
9
|
+
## Features
|
10
|
+
|
11
|
+
- Complete AT Protocol OAuth 2.0 implementation
|
12
|
+
- Secure by default with mandatory DPoP and PKCE
|
13
|
+
- Support for confidential (backend) and public clients
|
14
|
+
- Thread-safe session and token management
|
15
|
+
- Comprehensive identity resolution and verification
|
16
|
+
- Automatic token refresh and session management
|
17
|
+
- Robust error handling and recovery mechanisms
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'atproto_auth'
|
25
|
+
```
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
|
29
|
+
```sh
|
30
|
+
bundle install
|
31
|
+
```
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
```sh
|
36
|
+
gem install atproto_auth
|
37
|
+
```
|
38
|
+
|
39
|
+
## Requirements
|
40
|
+
|
41
|
+
- Ruby 3.0 or higher
|
42
|
+
- OpenSSL support
|
43
|
+
- For confidential clients: HTTPS-capable domain for client metadata hosting
|
44
|
+
|
45
|
+
## Basic Usage
|
46
|
+
|
47
|
+
### Configuration
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
require 'atproto_auth'
|
51
|
+
|
52
|
+
AtprotoAuth.configure do |config|
|
53
|
+
# Configure HTTP client settings
|
54
|
+
config.http_client = AtprotoAuth::HttpClient.new(
|
55
|
+
timeout: 10,
|
56
|
+
verify_ssl: true
|
57
|
+
)
|
58
|
+
|
59
|
+
# Set token lifetimes
|
60
|
+
config.default_token_lifetime = 300 # 5 minutes
|
61
|
+
config.dpop_nonce_lifetime = 300 # 5 minutes
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
### Confidential Client Example
|
66
|
+
|
67
|
+
Here's a basic example of using the library in a confidential client application:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
# Initialize client with metadata
|
71
|
+
client = AtprotoAuth::Client.new(
|
72
|
+
client_id: "https://app.example.com/client-metadata.json",
|
73
|
+
redirect_uri: "https://app.example.com/callback",
|
74
|
+
metadata: {
|
75
|
+
# Your client metadata...
|
76
|
+
}
|
77
|
+
)
|
78
|
+
|
79
|
+
# Start authorization flow
|
80
|
+
auth = client.authorize(
|
81
|
+
handle: "user.bsky.social",
|
82
|
+
scope: "atproto"
|
83
|
+
)
|
84
|
+
|
85
|
+
# Use auth[:url] to redirect user
|
86
|
+
|
87
|
+
# Handle callback
|
88
|
+
tokens = client.handle_callback(
|
89
|
+
code: params[:code],
|
90
|
+
state: params[:state],
|
91
|
+
iss: params[:iss]
|
92
|
+
)
|
93
|
+
|
94
|
+
# Make authenticated requests
|
95
|
+
headers = client.auth_headers(
|
96
|
+
session_id: tokens[:session_id],
|
97
|
+
method: "GET",
|
98
|
+
url: "https://api.example.com/resource"
|
99
|
+
)
|
100
|
+
```
|
101
|
+
|
102
|
+
For a complete working example of a confidential client implementation, check out the example application in `examples/confidential_client/`. This Sinatra-based web application demonstrates:
|
103
|
+
- Complete OAuth flow implementation
|
104
|
+
- Session management
|
105
|
+
- DPoP token binding
|
106
|
+
- Making authenticated API requests
|
107
|
+
- Proper error handling
|
108
|
+
- Key generation and management
|
109
|
+
|
110
|
+
See `examples/confidential_client/README.md` for setup instructions and implementation details.
|
111
|
+
|
112
|
+
### Public Client Example
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
client = AtprotoAuth::Client.new(
|
116
|
+
client_id: "https://app.example.com/client-metadata.json",
|
117
|
+
redirect_uri: "https://app.example.com/callback"
|
118
|
+
)
|
119
|
+
|
120
|
+
# Browser will open authorization URL
|
121
|
+
auth = client.authorize(
|
122
|
+
handle: "user.bsky.social",
|
123
|
+
scope: "atproto"
|
124
|
+
)
|
125
|
+
|
126
|
+
# After callback, exchange code for tokens
|
127
|
+
tokens = client.handle_callback(
|
128
|
+
code: params[:code],
|
129
|
+
state: params[:state],
|
130
|
+
iss: params[:iss]
|
131
|
+
)
|
132
|
+
```
|
133
|
+
|
134
|
+
## Features In Detail
|
135
|
+
|
136
|
+
### Identity Resolution
|
137
|
+
|
138
|
+
The library handles the complete AT Protocol identity resolution flow:
|
139
|
+
|
140
|
+
- Handle to DID resolution (DNS-based or HTTP fallback)
|
141
|
+
- DID document fetching and validation
|
142
|
+
- PDS (Personal Data Server) location verification
|
143
|
+
- Bidirectional handle verification
|
144
|
+
- Authorization server binding verification
|
145
|
+
|
146
|
+
### Token & Session Management
|
147
|
+
|
148
|
+
Comprehensive token lifecycle management:
|
149
|
+
|
150
|
+
- Secure token storage with encryption
|
151
|
+
- Automatic token refresh
|
152
|
+
- DPoP proof generation and binding
|
153
|
+
- Session state tracking
|
154
|
+
- Cleanup of expired sessions
|
155
|
+
|
156
|
+
### Security Features
|
157
|
+
|
158
|
+
Built-in security best practices:
|
159
|
+
|
160
|
+
- Mandatory PKCE for all flows
|
161
|
+
- DPoP token binding
|
162
|
+
- Constant-time token comparisons
|
163
|
+
- Thread-safe state management
|
164
|
+
- Protection against SSRF attacks
|
165
|
+
- Secure token storage
|
166
|
+
|
167
|
+
## Development
|
168
|
+
|
169
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
170
|
+
|
171
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
172
|
+
|
173
|
+
## Contributing
|
174
|
+
|
175
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jhuckabee/atproto_auth.
|
176
|
+
|
177
|
+
## License
|
178
|
+
|
179
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "rubocop/rake_task"
|
13
|
+
|
14
|
+
RuboCop::RakeTask.new
|
15
|
+
|
16
|
+
task default: %i[test rubocop]
|
@@ -0,0 +1,84 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../..
|
3
|
+
specs:
|
4
|
+
atproto_auth (0.1.0)
|
5
|
+
jose (~> 1.2)
|
6
|
+
jwt (~> 2.9)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
base64 (0.2.0)
|
12
|
+
concurrent-ruby (1.3.4)
|
13
|
+
dotenv (3.1.4)
|
14
|
+
faraday (2.12.1)
|
15
|
+
faraday-net_http (>= 2.0, < 3.5)
|
16
|
+
json
|
17
|
+
logger
|
18
|
+
faraday-net_http (3.4.0)
|
19
|
+
net-http (>= 0.5.0)
|
20
|
+
immutable-ruby (0.2.0)
|
21
|
+
concurrent-ruby (~> 1.1)
|
22
|
+
sorted_set (~> 1.0)
|
23
|
+
jose (1.2.0)
|
24
|
+
base64
|
25
|
+
immutable-ruby
|
26
|
+
json (2.9.0)
|
27
|
+
jwt (2.9.3)
|
28
|
+
base64
|
29
|
+
logger (1.6.2)
|
30
|
+
multi_json (1.15.0)
|
31
|
+
mustermann (3.0.3)
|
32
|
+
ruby2_keywords (~> 0.0.1)
|
33
|
+
net-http (0.6.0)
|
34
|
+
uri
|
35
|
+
nio4r (2.7.4)
|
36
|
+
puma (6.5.0)
|
37
|
+
nio4r (~> 2.0)
|
38
|
+
rack (3.1.8)
|
39
|
+
rack-protection (4.1.1)
|
40
|
+
base64 (>= 0.1.0)
|
41
|
+
logger (>= 1.6.0)
|
42
|
+
rack (>= 3.0.0, < 4)
|
43
|
+
rack-session (2.0.0)
|
44
|
+
rack (>= 3.0.0)
|
45
|
+
rackup (2.2.1)
|
46
|
+
rack (>= 3)
|
47
|
+
rbtree (0.4.6)
|
48
|
+
ruby2_keywords (0.0.5)
|
49
|
+
set (1.1.1)
|
50
|
+
sinatra (4.1.1)
|
51
|
+
logger (>= 1.6.0)
|
52
|
+
mustermann (~> 3.0)
|
53
|
+
rack (>= 3.0.0, < 4)
|
54
|
+
rack-protection (= 4.1.1)
|
55
|
+
rack-session (>= 2.0.0, < 3)
|
56
|
+
tilt (~> 2.0)
|
57
|
+
sinatra-contrib (4.1.1)
|
58
|
+
multi_json (>= 0.0.2)
|
59
|
+
mustermann (~> 3.0)
|
60
|
+
rack-protection (= 4.1.1)
|
61
|
+
sinatra (= 4.1.1)
|
62
|
+
tilt (~> 2.0)
|
63
|
+
sorted_set (1.0.3)
|
64
|
+
rbtree
|
65
|
+
set (~> 1.0)
|
66
|
+
tilt (2.4.0)
|
67
|
+
uri (1.0.2)
|
68
|
+
|
69
|
+
PLATFORMS
|
70
|
+
arm64-darwin-24
|
71
|
+
ruby
|
72
|
+
|
73
|
+
DEPENDENCIES
|
74
|
+
atproto_auth!
|
75
|
+
dotenv
|
76
|
+
faraday
|
77
|
+
json
|
78
|
+
puma
|
79
|
+
rackup
|
80
|
+
sinatra
|
81
|
+
sinatra-contrib
|
82
|
+
|
83
|
+
BUNDLED WITH
|
84
|
+
2.5.23
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# AT Protocol OAuth Confidential Client Example
|
2
|
+
|
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
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
The example implements a simple web application using Sinatra that:
|
8
|
+
- Allows users to sign in with their AT Protocol handle (@handle)
|
9
|
+
- Implements the complete OAuth authorization flow
|
10
|
+
- Uses DPoP-bound tokens for API requests
|
11
|
+
- Demonstrates secure session management
|
12
|
+
- Shows how to make authenticated API calls to Bluesky
|
13
|
+
|
14
|
+
## Requirements
|
15
|
+
|
16
|
+
- Ruby 3.0+
|
17
|
+
- Bundler
|
18
|
+
- A domain name for your application that matches your client metadata
|
19
|
+
- SSL certificate for your domain (required for production)
|
20
|
+
|
21
|
+
## Setup
|
22
|
+
|
23
|
+
1. Clone the repository and navigate to the example directory:
|
24
|
+
```bash
|
25
|
+
cd examples/confidential_client
|
26
|
+
```
|
27
|
+
|
28
|
+
2. Install dependencies:
|
29
|
+
```bash
|
30
|
+
bundle install
|
31
|
+
```
|
32
|
+
|
33
|
+
3. Generate EC keys for client authentication:
|
34
|
+
```bash
|
35
|
+
bundle exec ruby scripts/generate_keys.rb > config/keys.json
|
36
|
+
```
|
37
|
+
|
38
|
+
4. Configure your client metadata in `config/client-metadata.json`. Make sure to:
|
39
|
+
- Set the correct `client_id` URL where your metadata will be hosted
|
40
|
+
- Configure valid `redirect_uris` for your application
|
41
|
+
- Add your generated keys from step 3 to the `jwks` field
|
42
|
+
|
43
|
+
5. Set up environment variables:
|
44
|
+
```bash
|
45
|
+
export SESSION_SECRET=your-secure-session-secret # Required for session encryption
|
46
|
+
export PERMITTED_DOMAIN=your.domain.com # Your application's domain name
|
47
|
+
```
|
48
|
+
|
49
|
+
## Configuration
|
50
|
+
|
51
|
+
### Host Authorization
|
52
|
+
|
53
|
+
This application requires specific domain configuration to function properly:
|
54
|
+
|
55
|
+
1. **Domain-Client Matching**: The domain where you run the application must exactly match the `client_id` domain in your client metadata. For example, if your `client_id` is `https://myapp.example.com/client-metadata.json`, the application must be accessible at `myapp.example.com`.
|
56
|
+
|
57
|
+
2. **Internet Accessibility**: The application must be accessible from the internet for AT Protocol OAuth to work. The Authorization Server needs to be able to reach your application's redirect URI during the OAuth flow.
|
58
|
+
|
59
|
+
3. **Quick Setup with Tailscale Funnel**: One easy way to expose your local development server to the internet is using Tailscale Funnel:
|
60
|
+
|
61
|
+
1. Set up [Tailscale Funnel](https://tailscale.com/kb/1223/funnel)
|
62
|
+
2. Ensure you have HTTPS certificates configured
|
63
|
+
5. Start your Funnel:
|
64
|
+
```bash
|
65
|
+
tailscale funnel 9292
|
66
|
+
```
|
67
|
+
4. Ensure your client_id and redirect_uris match your funnel path
|
68
|
+
5. Set the `PERMITTED_DOMAIN` environment variable to your Tailscale domain
|
69
|
+
```bash
|
70
|
+
export PERMITTED_DOMAIN=machinename.xyz.ts.net
|
71
|
+
```
|
72
|
+
6. Run the application (see below).
|
73
|
+
|
74
|
+
Your application will now be accessible via your Tailscale domain with HTTPS enabled.
|
75
|
+
|
76
|
+
### Session Security
|
77
|
+
|
78
|
+
The application uses encrypted sessions to store authorization data. Configure the session secret with:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
export SESSION_SECRET=your-secure-random-string
|
82
|
+
```
|
83
|
+
|
84
|
+
If not set, a random secret will be generated on startup.
|
85
|
+
|
86
|
+
## Running the Application
|
87
|
+
|
88
|
+
```bash
|
89
|
+
bundle exec rackup
|
90
|
+
```
|
91
|
+
|
92
|
+
This will start the server on `http://localhost:9292`.
|
93
|
+
|
94
|
+
|
95
|
+
## Troubleshooting
|
96
|
+
|
97
|
+
### Common Issues
|
98
|
+
|
99
|
+
1. "Invalid redirect URI":
|
100
|
+
- Ensure your redirect URI matches exactly what's in your client metadata
|
101
|
+
- Check that your domain matches the client_id domain
|
102
|
+
|
103
|
+
2. "Invalid client metadata":
|
104
|
+
- Verify your client metadata is accessible at the URL specified in client_id
|
105
|
+
- Check that your JSON is valid and contains all required fields
|
106
|
+
|
107
|
+
3. "Authorization failed":
|
108
|
+
- Verify your JWKS configuration
|
109
|
+
- Check that your DPoP proofs are being generated correctly
|
110
|
+
- Ensure your client authentication is working
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sinatra/base"
|
4
|
+
require "sinatra/reloader"
|
5
|
+
require "atproto_auth"
|
6
|
+
require "faraday"
|
7
|
+
require "json"
|
8
|
+
require "dotenv/load"
|
9
|
+
|
10
|
+
# Main app entry point
|
11
|
+
class ExampleApp < Sinatra::Base
|
12
|
+
configure :development do
|
13
|
+
register Sinatra::Reloader
|
14
|
+
end
|
15
|
+
|
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
|
+
# Initialize the AT Protocol OAuth client
|
24
|
+
configure do
|
25
|
+
# Configure AtprotoAuth settings
|
26
|
+
AtprotoAuth.configure do |config|
|
27
|
+
config.http_client = AtprotoAuth::HttpClient.new(
|
28
|
+
timeout: 10,
|
29
|
+
verify_ssl: true
|
30
|
+
)
|
31
|
+
config.default_token_lifetime = 300
|
32
|
+
config.dpop_nonce_lifetime = 300
|
33
|
+
end
|
34
|
+
|
35
|
+
# Load client metadata
|
36
|
+
metadata_path = File.join(__dir__, "config", "client-metadata.json")
|
37
|
+
metadata = JSON.parse(File.read(metadata_path))
|
38
|
+
|
39
|
+
# Create OAuth client
|
40
|
+
set :oauth_client, AtprotoAuth::Client.new(
|
41
|
+
client_id: metadata["client_id"],
|
42
|
+
redirect_uri: metadata["redirect_uris"][0],
|
43
|
+
metadata: metadata,
|
44
|
+
dpop_key: metadata["jwks"]["keys"][0]
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
get "/" do
|
49
|
+
erb :index
|
50
|
+
end
|
51
|
+
|
52
|
+
# Start OAuth flow
|
53
|
+
post "/auth" do
|
54
|
+
handle = params[:handle]
|
55
|
+
|
56
|
+
begin
|
57
|
+
# Start authorization flow
|
58
|
+
auth = settings.oauth_client.authorize(
|
59
|
+
handle: handle,
|
60
|
+
scope: "atproto"
|
61
|
+
)
|
62
|
+
|
63
|
+
# Store session ID for callback
|
64
|
+
session[:oauth_session_id] = auth[:session_id]
|
65
|
+
|
66
|
+
# Redirect to authorization URL
|
67
|
+
redirect auth[:url]
|
68
|
+
rescue StandardError => e
|
69
|
+
session[:error] = "Authorization failed: #{e.message}"
|
70
|
+
redirect "/"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# OAuth callback handler
|
75
|
+
get "/callback" do
|
76
|
+
# Handle the callback
|
77
|
+
result = settings.oauth_client.handle_callback(
|
78
|
+
code: params[:code],
|
79
|
+
state: params[:state],
|
80
|
+
iss: params[:iss]
|
81
|
+
)
|
82
|
+
|
83
|
+
# Store tokens in session
|
84
|
+
session[:tokens] = result
|
85
|
+
|
86
|
+
redirect "/authorized"
|
87
|
+
rescue StandardError => e
|
88
|
+
session[:error] = "Callback failed: #{e.message}"
|
89
|
+
redirect "/"
|
90
|
+
end
|
91
|
+
|
92
|
+
# Show authorized state and test API call
|
93
|
+
get "/authorized" do
|
94
|
+
return redirect "/" unless session[:tokens]
|
95
|
+
|
96
|
+
begin
|
97
|
+
# Make test API call to com.atproto.identity.resolveHandle
|
98
|
+
conn = Faraday.new(url: "https://api.bsky.app") do |f|
|
99
|
+
f.request :json
|
100
|
+
f.response :json
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get auth headers for request
|
104
|
+
headers = settings.oauth_client.auth_headers(
|
105
|
+
session_id: session[:tokens][:session_id],
|
106
|
+
method: "GET",
|
107
|
+
url: "https://api.bsky.app/xrpc/com.atproto.identity.resolveHandle"
|
108
|
+
)
|
109
|
+
|
110
|
+
# Make authenticated request
|
111
|
+
response = conn.get("/xrpc/com.atproto.identity.resolveHandle") do |req|
|
112
|
+
headers.each { |k, v| req.headers[k] = v }
|
113
|
+
req.params[:handle] = "bsky.app"
|
114
|
+
end
|
115
|
+
|
116
|
+
@api_result = response.body
|
117
|
+
erb :authorized
|
118
|
+
rescue StandardError => e
|
119
|
+
session[:error] = "API call failed: #{e.message}"
|
120
|
+
redirect "/"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
get "/signout" do
|
125
|
+
session.clear
|
126
|
+
session[:notice] = "Successfully signed out"
|
127
|
+
redirect "/"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Helper method to check if user is authenticated
|
131
|
+
def authenticated?
|
132
|
+
return false unless session[:tokens]
|
133
|
+
|
134
|
+
settings.oauth_client.authorized?(session[:tokens][:session_id])
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
{
|
2
|
+
"client_id": "https://mac.tail7f768.ts.net/client-metadata.json",
|
3
|
+
"client_name": "AT Protocol OAuth Ruby Example",
|
4
|
+
"redirect_uris": ["https://mac.tail7f768.ts.net/callback"],
|
5
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
6
|
+
"response_types": ["code"],
|
7
|
+
"scope": "atproto",
|
8
|
+
"token_endpoint_auth_method": "private_key_jwt",
|
9
|
+
"token_endpoint_auth_signing_alg": "ES256",
|
10
|
+
"application_type": "web",
|
11
|
+
"dpop_bound_access_tokens": true,
|
12
|
+
"jwks": {
|
13
|
+
"keys": [
|
14
|
+
{
|
15
|
+
"use": "sig",
|
16
|
+
"kid": "key-1",
|
17
|
+
"x": "SzXlDk9rSyrZ3b0fVKOWFYY-AFZtld2zElycsmDZ3Xk",
|
18
|
+
"crv": "P-256",
|
19
|
+
"d": "OLJJKo9T9W7taz8gFd5YdsBw8cOpv3p5zPPtv2XaKcM",
|
20
|
+
"kty": "EC",
|
21
|
+
"y": "4hIBLl-BLD1Ypk-mvPxT2OR52ezMs4XI1MGBdhlLLm4"
|
22
|
+
}
|
23
|
+
]
|
24
|
+
}
|
25
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
{
|
2
|
+
"client_id": "https://mac.tail7f768.ts.net/client-metadata.json",
|
3
|
+
"client_name": "AT Protocol OAuth Ruby Example",
|
4
|
+
"redirect_uris": ["https://mac.tail7f768.ts.net/callback"],
|
5
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
6
|
+
"response_types": ["code"],
|
7
|
+
"scope": "atproto",
|
8
|
+
"token_endpoint_auth_method": "private_key_jwt",
|
9
|
+
"token_endpoint_auth_signing_alg": "ES256",
|
10
|
+
"application_type": "web",
|
11
|
+
"dpop_bound_access_tokens": true,
|
12
|
+
"jwks": {
|
13
|
+
"keys": [
|
14
|
+
{
|
15
|
+
"use": "sig",
|
16
|
+
"kid": "key-1",
|
17
|
+
"x": "SzXlDk9rSyrZ3b0fVKOWFYY-AFZtld2zElycsmDZ3Xk",
|
18
|
+
"crv": "P-256",
|
19
|
+
"kty": "EC",
|
20
|
+
"y": "4hIBLl-BLD1Ypk-mvPxT2OR52ezMs4XI1MGBdhlLLm4"
|
21
|
+
}
|
22
|
+
]
|
23
|
+
}
|
24
|
+
}
|