blue_factory 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1a078159ccaad626c030e20a9ad18e1a408cc8897c27cddb9773c1bd1a6295c
4
- data.tar.gz: b7c45df10d3419f21745b760b586fe21e1eb262e9a12c72a5282e3ca0616eace
3
+ metadata.gz: a7f8f8505228a075534b74c48c3011441aca5f8710bb1dc6fd769fd62cb60d71
4
+ data.tar.gz: 303454ba19fee635367049603810cb1ebb7662665ccc05ce99c035b063c94b2c
5
5
  SHA512:
6
- metadata.gz: 3347004aff37324480e13d2c1a21b85f737a9ab8898d874ecf26dad6f1b8f74ffa621ca24d4d76098be1479e145c08745d8c86a5cfcab68e66c8148bd6f76a8c
7
- data.tar.gz: 15fd3de8b85ecb2175c3d3ede61b73257bac08e849935ad42aad6b428d70c1fbe1fbec970887db354a6fea8f36fa93851f2909452497cc33f50419bff4cd1bea
6
+ metadata.gz: 17b3606d68a5bc89557e78fc85478319499e84ce903fdc2f4e56a8221f923435f0c5a55b43a1a64fc974dc843b38d6d9a672480e56fe12c3f5eb6e26fbb5cff6
7
+ data.tar.gz: b53d4ac56b668b15f256bf5a87abd8f8195d0a91e3f7308b78f24454be9d436a05c539fb31b14fd6febe3b6a7621b7e151ef2c90b3963c64d52c743bd30bb7c6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [0.1.4] - 2023-08-19
2
+
3
+ - implemented partial authentication, without signature verification (`enable_unsafe_auth` option)
4
+
1
5
  ## [0.1.3] - 2023-07-27
2
6
 
3
7
  - fixed incorrect response when reaching the end of the feed
data/README.md CHANGED
@@ -40,7 +40,8 @@ BlueFactory.add_feed 'starwars', StarWarsFeed.new
40
40
 
41
41
  The `get_posts` method of the feed object should:
42
42
 
43
- - accept a single `params` argument which is a hash with fields: `:feed`, `:cursor` and `:limit` (the last two are optional)
43
+ - accept a `params` argument which is a hash with fields: `:feed`, `:cursor` and `:limit` (the last two are optional)
44
+ - optionally, accept a second `current_user` argument which is a string with the authenticated user's DID (depends on authentication config - [see below](#authentication))
44
45
  - return a hash with two fields: `:cursor` and `:posts`
45
46
 
46
47
  The `:feed` is the `at://` URI of the feed. The `:cursor` param, if included, should be a cursor returned by your feed from one of the previous requests, so it should be in the format used by the same function - but anyone can call the endpoint with any params, so you should validate it. The cursor is used for pagination to provide more pages further down in the feed (the first request to load the top of the feed doesn't include a cursor).
@@ -59,7 +60,7 @@ An example implementation could look like this:
59
60
  require 'time'
60
61
 
61
62
  class StarWarsFeed
62
- def get_posts(params)
63
+ def get_posts(params, current_user = nil)
63
64
  limit = check_query_limit(params)
64
65
  query = Post.select('uri, time').order('time DESC').limit(limit)
65
66
 
@@ -122,7 +123,54 @@ server {
122
123
  }
123
124
  ```
124
125
 
125
- ### Additional configuration & customizing
126
+ ## Authentication
127
+
128
+ Feeds are authenticated using [JSON Web Tokens](https://jwt.io). When a user opens, refreshes or scrolls down a feed in their app, a request is made to the feed service from the Bluesky network's IP address with user's authentication token in the `Authorization` HTTP header.
129
+
130
+ At the moment, Blue Factory handles authentication in a very simplified way - it extracts the user's DID from the authentication header, but it does not verify the signature. This means that anyone can trivially prepare a fake token and make requests to the `getFeedSkeleton` endpoint as a different user.
131
+
132
+ As such, this authentication should not be used for anything critical. It may be used for things like logging, analytics, or as "security by obscurity" to just discourage others from accessing the feed in the app.
133
+
134
+ To use this simple authentication, set the `enable_unsafe_auth` option:
135
+
136
+ ```rb
137
+ BlueFactory.set :enable_unsafe_auth, true
138
+ ```
139
+
140
+ The user's DID extracted from the token is passed as a second argument to `#get_posts`. You may, for example, return an empty list when the user is not authorized to use it:
141
+
142
+ ```rb
143
+ class HiddenFeed
144
+ def get_posts(params, current_user)
145
+ if AUTHORIZED_USERS.include?(current_user)
146
+ # ...
147
+ else
148
+ { posts: [] }
149
+ end
150
+ end
151
+ end
152
+ ```
153
+
154
+ Alternatively, you can raise a `BlueFactory::AuthorizationError` with an optional custom message. This will return a 401 status response to the Bluesky app, which will make it display the pink error banner in the app:
155
+
156
+ ```rb
157
+ class HiddenFeed
158
+ def get_posts(params, current_user)
159
+ if AUTHORIZED_USERS.include?(current_user)
160
+ # ...
161
+ else
162
+ raise BlueFactory::AuthorizationError, "You shall not pass!"
163
+ end
164
+ end
165
+ end
166
+ ```
167
+
168
+ <p><img width="400" src="https://github.com/mackuba/blue_factory/assets/28465/9197c0ec-9302-4ca0-b06c-3fce2e0fa4f4"></p>
169
+
170
+ Note: the `current_user` may be `nil` if the authentication header is not set at all (which may happen if you access the endpoint e.g. with `curl` or in a browser).
171
+
172
+
173
+ ## Additional configuration & customizing
126
174
 
127
175
  You can use the [Sinatra API](https://sinatrarb.com/intro.html#configuration) to do any additional configuration, like changing the server port, enabling/disabling logging and so on.
128
176
 
@@ -13,7 +13,7 @@ module BlueFactory
13
13
  (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
14
14
  end
15
15
 
16
- configurable :publisher_did, :hostname, :validate_responses
16
+ configurable :publisher_did, :hostname, :validate_responses, :enable_unsafe_auth
17
17
 
18
18
  set :validate_responses, (environment != :production)
19
19
  end
@@ -1,4 +1,13 @@
1
1
  module BlueFactory
2
+ class AuthorizationError < StandardError
3
+ attr_reader :error_type
4
+
5
+ def initialize(message = "Authentication required", error_type = nil)
6
+ super(message)
7
+ @error_type = error_type
8
+ end
9
+ end
10
+
2
11
  class InvalidKeyError < StandardError
3
12
  end
4
13
 
@@ -13,4 +22,7 @@ module BlueFactory
13
22
 
14
23
  class InvalidResponseError < StandardError
15
24
  end
25
+
26
+ class UnsupportedAlgorithmError < StandardError
27
+ end
16
28
  end
@@ -1,3 +1,4 @@
1
+ require 'base64'
1
2
  require 'json'
2
3
  require 'sinatra/base'
3
4
 
@@ -33,6 +34,48 @@ module BlueFactory
33
34
  [status, JSON.generate({ error: name, message: message })]
34
35
  end
35
36
 
37
+ def get_feed
38
+ if params[:feed].to_s.empty?
39
+ raise InvalidResponseError, "Error: Params must have the property \"feed\""
40
+ end
41
+
42
+ if params[:feed] !~ %r(^at://[\w\-\.\:]+/[\w\.]+/[\w\.\-]+$)
43
+ raise InvalidResponseError, "Error: feed must be a valid at-uri"
44
+ end
45
+
46
+ feed_key = params[:feed].split('/').last
47
+ feed = config.get_feed(feed_key)
48
+
49
+ if feed.nil? || feed_uri(feed_key) != params[:feed]
50
+ raise UnsupportedAlgorithmError, "Unsupported algorithm"
51
+ end
52
+
53
+ feed
54
+ end
55
+
56
+ def parse_did_from_token
57
+ auth = env['HTTP_AUTHORIZATION']
58
+
59
+ if auth.to_s.strip.empty?
60
+ return nil
61
+ end
62
+
63
+ if !auth.start_with?('Bearer ')
64
+ raise AuthorizationError, "Unsupported authorization method"
65
+ end
66
+
67
+ token = auth.gsub(/^Bearer /, '')
68
+ parts = token.split('.')
69
+ raise AuthorizationError.new("Invalid JWT format", "BadJwt") unless parts.length == 3
70
+
71
+ begin
72
+ payload = JSON.parse(Base64.decode64(parts[1]))
73
+ payload['iss']
74
+ rescue StandardError => e
75
+ raise AuthorizationError.new("Invalid JWT format", "BadJwt")
76
+ end
77
+ end
78
+
36
79
  def validate_response(response)
37
80
  cursor = response[:cursor]
38
81
  raise InvalidResponseError, ":cursor key is missing" unless response.has_key?(:cursor)
@@ -50,23 +93,17 @@ module BlueFactory
50
93
  end
51
94
 
52
95
  get '/xrpc/app.bsky.feed.getFeedSkeleton' do
53
- if params[:feed].to_s.empty?
54
- return json_error("InvalidRequest", "Error: Params must have the property \"feed\"")
55
- end
56
-
57
- if params[:feed] !~ %r(^at://[\w\-\.\:]+/[\w\.]+/[\w\.\-]+$)
58
- return json_error("InvalidRequest", "Error: feed must be a valid at-uri")
59
- end
60
-
61
- feed_key = params[:feed].split('/').last
62
- feed = config.get_feed(feed_key)
63
-
64
- if feed.nil? || feed_uri(feed_key) != params[:feed]
65
- return json_error("UnsupportedAlgorithm", "Unsupported algorithm")
66
- end
67
-
68
96
  begin
69
- response = feed.get_posts(params.slice(:feed, :cursor, :limit))
97
+ feed = get_feed
98
+ args = params.slice(:feed, :cursor, :limit)
99
+
100
+ if config.enable_unsafe_auth
101
+ did = parse_did_from_token
102
+ response = feed.get_posts(args, did)
103
+ else
104
+ response = feed.get_posts(args)
105
+ end
106
+
70
107
  validate_response(response) if config.validate_responses
71
108
 
72
109
  output = {}
@@ -76,6 +113,10 @@ module BlueFactory
76
113
  return json(output)
77
114
  rescue InvalidRequestError => e
78
115
  return json_error(e.error_type || "InvalidRequest", e.message)
116
+ rescue AuthorizationError => e
117
+ return json_error(e.error_type || "AuthenticationRequired", e.message, status: 401)
118
+ rescue UnsupportedAlgorithmError => e
119
+ return json_error("UnsupportedAlgorithm", e.message)
79
120
  rescue InvalidResponseError => e
80
121
  return json_error("InvalidResponse", e.message)
81
122
  end
@@ -1,3 +1,3 @@
1
1
  module BlueFactory
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blue_factory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuba Suder
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-27 00:00:00.000000000 Z
11
+ date: 2023-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sinatra