omniauth-atproto 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5e2f34cc5f324628d5f73659d4cd3ea11cbfbcdc5420cefaa78908c928008d7a
4
+ data.tar.gz: 1e349da8447a8d95b29a8f0c8965d13c1cdf5525e84ecdb02a2bf69b253de82e
5
+ SHA512:
6
+ metadata.gz: d71697e0c667218a143ad87f2bf30098e6766365cb6bef90709ffdb004bffdff3edd000c332c8124279c3dd63c4cc9a47748e3658c23124ee629a41e9eddbd4d
7
+ data.tar.gz: fafbab85910a7b064e9f4ef118bc9c5f0df5d900a4ca7b8c9c01286a76ccc4c88c0bbb1ceb96d174d3db5d71de3acbdb396d3ee62dbe57a9c90d4b193d941dc4
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+
2
+
3
+ # Omniauth-atproto
4
+
5
+ An omniauth strategy for Atproto (bluesky)
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'omniauth-atproto'
13
+ ```
14
+
15
+
16
+ ## Usage
17
+
18
+ You can cnfigure it :
19
+ ```ruby
20
+ Rails.application.config.middleware.use OmniAuth::Builder do
21
+ provider(:atproto,
22
+ "#{Rails.application.config.app_url}/oauth/client-metadata.json",
23
+ nil,
24
+ client_options: {
25
+ site: "https://bsky.social",
26
+ authorize_url: "https://bsky.social/oauth/authorize",
27
+ token_url: "https://bsky.social/oauth/token"
28
+ },
29
+ scope: "atproto transition:generic",
30
+ private_key: OmniAuth::Atproto::KeyManager.current_private_key,
31
+ client_jwk: OmniAuth::Atproto::KeyManager.current_jwk)
32
+ end
33
+ ```
34
+ You will have to generate keys and the oauth/client-metadata.json document (a generator should come soon)
35
+
36
+ ```ruby
37
+ #lib/tasks/atproto.rake
38
+ :atproto do
39
+ desc "Generate new AtProto key pair and rotate keys"
40
+ task rotate_keys: :environment do
41
+ OmniAuth::Atproto::KeyManager.rotate_keys
42
+ puts "New key generated and saved. Old key backed up if it existed."
43
+ Rake::Task["atproto:generate_metadata"].invoke
44
+ end
45
+
46
+ desc "Generate client metadata JSON file"
47
+ task generate_metadata: :environment do
48
+ metadata = {
49
+ client_id: "#{Rails.application.config.app_url}/oauth/client-metadata.json",
50
+ application_type: "web",
51
+ client_name: Rails.application.class.module_parent_name,
52
+ client_uri: Rails.application.config.app_url,
53
+ dpop_bound_access_tokens: true,
54
+ grant_types: %w[authorization_code refresh_token],
55
+ redirect_uris: [ "#{Rails.application.config.app_url}/auth/atproto/callback" ],
56
+ response_types: [ "code" ],
57
+ scope: "atproto transition:generic",
58
+ token_endpoint_auth_method: "private_key_jwt",
59
+ token_endpoint_auth_signing_alg: "ES256",
60
+ jwks: {
61
+ keys: [ OmniAuth::Atproto::KeyManager.current_jwk ]
62
+ }
63
+ }
64
+
65
+ oauth_dir = Rails.root.join("public", "oauth")
66
+ FileUtils.mkdir_p(oauth_dir) unless Dir.exist?(oauth_dir)
67
+ metadata_path = oauth_dir.join("client-metadata.json")
68
+ File.write(metadata_path, JSON.pretty_generate(metadata))
69
+ puts "Generated metadata file at #{metadata_path}"
70
+ end
71
+ end
72
+ ```
73
+ Then you can
74
+ ```bash
75
+ rails atproto:generate_metadata
76
+ ```
77
+ The values from the metadata endpoint should correspond to those you gave as option for the strategy (that's why a generator would be very handy)
@@ -0,0 +1,86 @@
1
+ require 'omniauth-oauth2'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'atproto_client'
5
+
6
+ module OmniAuth
7
+ module Strategies
8
+ class Atproto < OmniAuth::Strategies::OAuth2
9
+ def initialize(app, *args)
10
+ super
11
+ @dpop_handler = AtProto::DpopHandler.new(options.private_key)
12
+ end
13
+
14
+ option :scope, 'atproto'
15
+ option :pkce, true
16
+ option :token_params, {
17
+ test: true
18
+ }
19
+
20
+ info do
21
+ {
22
+ did: @access_token.params['sub'],
23
+ pds_host: options.client_options.site
24
+ }
25
+ end
26
+
27
+ private
28
+
29
+ def build_access_token
30
+ new_token_params = token_params.merge(
31
+ {
32
+ grant_type: 'authorization_code',
33
+ redirect_uri: full_host + callback_path,
34
+ code: request.params['code'],
35
+ client_id: options.client_id,
36
+ client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
37
+ client_assertion: generate_client_assertion
38
+ }
39
+ )
40
+ response = @dpop_handler.make_request(
41
+ client.token_url,
42
+ :post,
43
+ headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/json' },
44
+ body: new_token_params
45
+ )
46
+
47
+ ::OAuth2::AccessToken.from_hash(client, response)
48
+ end
49
+
50
+ def generate_client_assertion
51
+ # Should return a JWT signed with the private key corresponding to the one in client-metadata.json
52
+
53
+ raise 'Client ID is required' unless options.client_id
54
+ raise 'Client JWK is required' unless options.client_jwk
55
+
56
+ private_key = if options.private_key.is_a?(String)
57
+ OpenSSL::PKey::EC.new(options.private_key)
58
+ elsif options.private_key.is_a?(OpenSSL::PKey::EC)
59
+ options.private_key
60
+ else
61
+ raise 'Invalid private_key format'
62
+ end
63
+
64
+ jwt_payload = {
65
+ iss: options.client_id,
66
+ sub: options.client_id,
67
+ aud: options.client_options.site,
68
+ jti: SecureRandom.uuid,
69
+ iat: Time.now.to_i,
70
+ exp: Time.now.to_i + 300
71
+ }
72
+
73
+ JWT.encode(
74
+ jwt_payload,
75
+ private_key,
76
+ 'ES256',
77
+ {
78
+ typ: 'jwt',
79
+ alg: 'ES256',
80
+ kid: options.client_jwk[:kid]
81
+ }
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,77 @@
1
+ require 'openssl'
2
+ require 'jwt'
3
+ require 'base64'
4
+
5
+ module OmniAuth
6
+ module Atproto
7
+ class KeyManager
8
+ class << self
9
+ KEY_PATH = 'config/atproto_private_key.pem'
10
+ JWK_PATH = 'config/atproto_jwk.json'
11
+
12
+ def generate_key_pair
13
+ key = OpenSSL::PKey::EC.generate('prime256v1')
14
+ private_key = key
15
+ public_key = key.public_key
16
+
17
+ # Get the coordinates for JWK
18
+ # (not easy with openssl 3)
19
+ bn = public_key.to_bn(:uncompressed)
20
+ raw_bytes = bn.to_s(2)
21
+ coord_bytes = raw_bytes[1..]
22
+ byte_length = coord_bytes.length / 2
23
+
24
+ x_coord = coord_bytes[0, byte_length]
25
+ y_coord = coord_bytes[byte_length, byte_length]
26
+
27
+ jwk = {
28
+ kty: 'EC',
29
+ crv: 'P-256',
30
+ x: Base64.urlsafe_encode64(x_coord, padding: false),
31
+ y: Base64.urlsafe_encode64(y_coord, padding: false),
32
+ use: 'sig',
33
+ alg: 'ES256',
34
+ kid: SecureRandom.uuid
35
+ }.freeze
36
+
37
+ [private_key, jwk]
38
+ end
39
+
40
+ def current_private_key
41
+ @current_private_key ||= load_or_generate_keys.first
42
+ end
43
+
44
+ def current_jwk
45
+ @current_jwk ||= load_or_generate_keys.last
46
+ end
47
+
48
+ def rotate_keys
49
+ # Backup current keys if they exist
50
+ if File.exist?(KEY_PATH)
51
+ File.write(KEY_PATH, 'config/old_atproto_private_key.pem')
52
+ FileUtils.rm(KEY_PATH)
53
+ end
54
+ if File.exist?(JWK_PATH)
55
+ File.write(JWK_PATH, 'config/old_atproto_jwk.json')
56
+ FileUtils.rm(JWK_PATH)
57
+ end
58
+ load_or_generate_keys
59
+ end
60
+
61
+ private
62
+
63
+ def load_or_generate_keys
64
+ if File.exist?(KEY_PATH) && File.exist?(JWK_PATH)
65
+ private_key = OpenSSL::PKey::EC.new(File.read(KEY_PATH))
66
+ jwk = JSON.parse(File.read(JWK_PATH), symbolize_names: true)
67
+ else
68
+ private_key, jwk = generate_key_pair
69
+ File.write(KEY_PATH, private_key.to_pem)
70
+ File.write(JWK_PATH, JSON.pretty_generate(jwk))
71
+ end
72
+ [private_key, jwk]
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,24 @@
1
+ module OmniAuth
2
+ module Atproto
3
+ class MetadataGenerator
4
+ def self.generate(options)
5
+ {
6
+ client_id: options[:client_id],
7
+ application_type: "web",
8
+ client_name: options[:client_name],
9
+ client_uri: options[:client_uri],
10
+ dpop_bound_access_tokens: true,
11
+ grant_types: ["authorization_code", "refresh_token"],
12
+ redirect_uris: [options[:redirect_uri]],
13
+ response_types: ["code"],
14
+ scope: options[:scope] || "atproto transition:generic",
15
+ token_endpoint_auth_method: "private_key_jwt",
16
+ token_endpoint_auth_signing_alg: "ES256",
17
+ jwks: {
18
+ keys: [options[:client_jwk]]
19
+ }
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module OmniAuth
2
+ module Atproto
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ require 'omniauth-oauth2'
2
+ require 'omniauth/strategies/atproto'
3
+ require 'omniauth-atproto/version'
4
+ require 'omniauth-atproto/key_manager'
5
+ require 'omniauth-atproto/metadata_generator'
6
+
7
+ module OmniAuth
8
+ module Atproto
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omniauth-atproto
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - frabr
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-11-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: atproto_client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: omniauth-oauth2
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: omniauth-rails_csrf_protection
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '2.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '13.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '13.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ description: OmniAuth strategy for authenticating with AtProto services like Bluesky
112
+ email:
113
+ - frabr@lasercats.fr
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - README.md
119
+ - lib/omniauth-atproto.rb
120
+ - lib/omniauth-atproto/key_manager.rb
121
+ - lib/omniauth-atproto/metadata_generator.rb
122
+ - lib/omniauth-atproto/version.rb
123
+ - lib/omniauth/strategies/atproto.rb
124
+ homepage: https://github.com/lasercats/omniauth-atproto
125
+ licenses:
126
+ - MIT
127
+ metadata:
128
+ homepage_uri: https://github.com/lasercats/omniauth-atproto
129
+ source_code_uri: https://github.com/lasercats/omniauth-atproto
130
+ changelog_uri: https://github.com/lasercats/omniauth-atproto/blob/master/CHANGELOG.md
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: 2.6.0
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubygems_version: 3.5.3
147
+ signing_key:
148
+ specification_version: 4
149
+ summary: OmniAuth strategy for AtProto
150
+ test_files: []