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
@@ -0,0 +1,69 @@
|
|
1
|
+
module AtprotoAuth
|
2
|
+
module ServerMetadata
|
3
|
+
class AuthorizationServer
|
4
|
+
@issuer: String
|
5
|
+
@authorization_endpoint: String
|
6
|
+
@token_endpoint: String
|
7
|
+
@pushed_authorization_request_endpoint: String
|
8
|
+
@response_types_supported: Array[String]
|
9
|
+
@grant_types_supported: Array[String]
|
10
|
+
@code_challenge_methods_supported: Array[String]
|
11
|
+
@token_endpoint_auth_methods_supported: Array[String]
|
12
|
+
@token_endpoint_auth_signing_alg_values_supported: Array[String]
|
13
|
+
@scopes_supported: Array[String]
|
14
|
+
@dpop_signing_alg_values_supported: Array[String]
|
15
|
+
|
16
|
+
REQUIRED_FIELDS: ::Array["issuer" | "authorization_endpoint" | "token_endpoint" | "response_types_supported" | "grant_types_supported" | "code_challenge_methods_supported" | "token_endpoint_auth_methods_supported" | "token_endpoint_auth_signing_alg_values_supported" | "scopes_supported" | "dpop_signing_alg_values_supported" | "pushed_authorization_request_endpoint"]
|
17
|
+
|
18
|
+
attr_reader issuer: String
|
19
|
+
attr_reader authorization_endpoint: String
|
20
|
+
attr_reader token_endpoint: String
|
21
|
+
attr_reader pushed_authorization_request_endpoint: String
|
22
|
+
attr_reader response_types_supported: Array[String]
|
23
|
+
attr_reader grant_types_supported: Array[String]
|
24
|
+
attr_reader code_challenge_methods_supported: Array[String]
|
25
|
+
attr_reader token_endpoint_auth_methods_supported: Array[String]
|
26
|
+
attr_reader token_endpoint_auth_signing_alg_values_supported: Array[String]
|
27
|
+
attr_reader scopes_supported: Array[String]
|
28
|
+
attr_reader dpop_signing_alg_values_supported: Array[String]
|
29
|
+
|
30
|
+
def initialize: (Hash[String, untyped] metadata) -> void
|
31
|
+
|
32
|
+
# Fetches and validates Authorization Server metadata from an issuer URL
|
33
|
+
# @param issuer [String] Authorization Server issuer URL
|
34
|
+
# @return [AuthorizationServer] new instance with fetched metadata
|
35
|
+
# @raise [InvalidAuthorizationServer] if metadata is invalid
|
36
|
+
def self.from_issuer: (String issuer) -> AuthorizationServer
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def validate_and_set_metadata!: (Hash[String, untyped] metadata) -> void
|
41
|
+
|
42
|
+
def validate_issuer!: (String issuer) -> String
|
43
|
+
|
44
|
+
def validate_https_url!: (String url) -> String
|
45
|
+
|
46
|
+
def validate_response_types!: (Array[String] types) -> void
|
47
|
+
|
48
|
+
def validate_grant_types!: (Array[String] types) -> void
|
49
|
+
|
50
|
+
def validate_code_challenge_methods!: (Array[String] methods) -> void
|
51
|
+
|
52
|
+
def validate_token_endpoint_auth_methods!: (Array[String] methods) -> void
|
53
|
+
|
54
|
+
def validate_token_endpoint_auth_signing_algs!: (Array[String] algs) -> void
|
55
|
+
|
56
|
+
def validate_dpop_signing_algs!: (Array[String] algs) -> void
|
57
|
+
|
58
|
+
def validate_scopes!: (Array[String] scopes) -> void
|
59
|
+
|
60
|
+
def validate_boolean_field!: (Hash[String, untyped] metadata, String field, bool required_value) -> void
|
61
|
+
|
62
|
+
def self.fetch_metadata: (String issuer) -> Hash[Symbol, String]
|
63
|
+
|
64
|
+
def self.parse_metadata: (String body) -> Hash[String, untyped]
|
65
|
+
|
66
|
+
def self.validate_issuer!: (String metadata_issuer, String request_issuer) -> void
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module AtprotoAuth
|
2
|
+
module ServerMetadata
|
3
|
+
class OriginUrl
|
4
|
+
@url: String
|
5
|
+
@uri: URI
|
6
|
+
|
7
|
+
attr_reader url: String
|
8
|
+
attr_reader uri: URI
|
9
|
+
|
10
|
+
def initialize: (String url) -> void
|
11
|
+
|
12
|
+
def valid?: () -> bool
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def uses_https_scheme?: () -> bool
|
17
|
+
def has_root_path?: () -> bool
|
18
|
+
def has_explicit_port?: () -> bool
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module AtprotoAuth
|
2
|
+
module ServerMetadata
|
3
|
+
class ResourceServer
|
4
|
+
@authorization_servers: Array[String]
|
5
|
+
|
6
|
+
attr_reader authorization_servers: Array[String]
|
7
|
+
|
8
|
+
def initialize: (Hash[String, untyped] metadata) -> void
|
9
|
+
|
10
|
+
def self.from_url: (String url) -> ResourceServer
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def validate_authorization_servers!: (Array[String] servers) -> Array[String]
|
15
|
+
|
16
|
+
def ensure_servers_exist: (Array[String] | nil) -> void
|
17
|
+
|
18
|
+
def ensure_exactly_one_server: (Array[String]) -> void
|
19
|
+
|
20
|
+
def validate_server_url_format: (String server_url) -> void
|
21
|
+
|
22
|
+
def self.fetch_metadata: (String url) -> Hash[Symbol, String]
|
23
|
+
|
24
|
+
def self.parse_metadata: (String body) -> Hash[String, untyped]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module AtprotoAuth
|
2
|
+
module State
|
3
|
+
class Session
|
4
|
+
@session_id: String
|
5
|
+
@state_token: String
|
6
|
+
@client_id: String
|
7
|
+
@scope: String
|
8
|
+
@auth_server: AuthorizationServer?
|
9
|
+
@did: String?
|
10
|
+
@pkce_verifier: String
|
11
|
+
@pkce_challenge: String
|
12
|
+
@tokens: TokenSet?
|
13
|
+
|
14
|
+
include MonitorMixin
|
15
|
+
|
16
|
+
attr_reader session_id: String
|
17
|
+
attr_reader state_token: String
|
18
|
+
attr_reader client_id: String
|
19
|
+
attr_reader scope: String
|
20
|
+
attr_reader pkce_verifier: String
|
21
|
+
attr_reader pkce_challenge: String
|
22
|
+
attr_reader auth_server: AuthorizationServer?
|
23
|
+
attr_reader did: String?
|
24
|
+
attr_reader tokens: TokenSet?
|
25
|
+
|
26
|
+
def initialize: (
|
27
|
+
client_id: String,
|
28
|
+
scope: String,
|
29
|
+
?auth_server: AuthorizationServer?,
|
30
|
+
?did: String?
|
31
|
+
) -> void
|
32
|
+
|
33
|
+
def authorization_server=: (AuthorizationServer server) -> void
|
34
|
+
|
35
|
+
def did=: (String did) -> void
|
36
|
+
|
37
|
+
def tokens=: (TokenSet tokens) -> void
|
38
|
+
|
39
|
+
def authorized?: () -> bool
|
40
|
+
|
41
|
+
def renewable?: () -> bool
|
42
|
+
|
43
|
+
def validate_state: (String state) -> bool
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def secure_compare: (String str1, String str2) -> bool
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module AtprotoAuth
|
2
|
+
module State
|
3
|
+
class SessionManager
|
4
|
+
@sessions: Hash[String, Session]
|
5
|
+
|
6
|
+
include MonitorMixin
|
7
|
+
|
8
|
+
def initialize: () -> void
|
9
|
+
|
10
|
+
def create_session: (
|
11
|
+
client_id: String,
|
12
|
+
scope: String,
|
13
|
+
?auth_server: ServerMetadata::AuthorizationServer?,
|
14
|
+
?did: String?
|
15
|
+
) -> Session
|
16
|
+
|
17
|
+
def get_session: (String session_id) -> Session?
|
18
|
+
|
19
|
+
def get_session_by_state: (String state) -> Session?
|
20
|
+
|
21
|
+
def remove_session: (String session_id) -> void
|
22
|
+
|
23
|
+
def cleanup_expired: () -> void
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module AtprotoAuth
|
2
|
+
module State
|
3
|
+
class TokenSet
|
4
|
+
@access_token: String
|
5
|
+
@refresh_token: String?
|
6
|
+
@token_type: String
|
7
|
+
@scope: String
|
8
|
+
@sub: String
|
9
|
+
@expires_at: Time
|
10
|
+
|
11
|
+
attr_reader access_token: String
|
12
|
+
attr_reader refresh_token: String?
|
13
|
+
attr_reader token_type: String
|
14
|
+
attr_reader scope: String
|
15
|
+
attr_reader expires_at: Time
|
16
|
+
attr_reader sub: String
|
17
|
+
|
18
|
+
def initialize: (
|
19
|
+
access_token: String,
|
20
|
+
token_type: String,
|
21
|
+
expires_in: Integer,
|
22
|
+
scope: String,
|
23
|
+
sub: String,
|
24
|
+
?refresh_token: String?
|
25
|
+
) -> void
|
26
|
+
|
27
|
+
def renewable?: () -> bool
|
28
|
+
|
29
|
+
def expired?: (?Integer buffer) -> bool
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def validate_token_type!: (String type) -> void
|
34
|
+
|
35
|
+
def validate_required!: (String name, String? value) -> void
|
36
|
+
|
37
|
+
def validate_expires_in!: (Integer expires_in) -> void
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
interface _HTTPClient
|
2
|
+
def get: (String, ?Hash[Symbol, untyped]) -> { status: Integer, body: String, headers: Hash[String, String] }
|
3
|
+
def post: (String, ?Hash[Symbol, untyped]) -> { status: Integer, body: String, headers: Hash[String, String] }
|
4
|
+
def put: (String, ?Hash[Symbol, untyped]) -> { status: Integer, body: String, headers: Hash[String, String] }
|
5
|
+
def delete: (String, ?Hash[Symbol, untyped]) -> { status: Integer, body: String, headers: Hash[String, String] }
|
6
|
+
end
|
7
|
+
|
8
|
+
module AtprotoAuth
|
9
|
+
class Error < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
class OAuthError
|
13
|
+
attr_reader error_code: String
|
14
|
+
|
15
|
+
def initialize: (String message, String error_code) -> void
|
16
|
+
end
|
17
|
+
|
18
|
+
class InvalidClientMetadata < OAuthError
|
19
|
+
def initialize: (String message) -> void
|
20
|
+
end
|
21
|
+
|
22
|
+
class InvalidAuthorizationServer < OAuthError
|
23
|
+
def initialize: (String message) -> void
|
24
|
+
end
|
25
|
+
|
26
|
+
class Configuration
|
27
|
+
attr_accessor default_token_lifetime: Integer
|
28
|
+
attr_accessor dpop_nonce_lifetime: Integer
|
29
|
+
attr_accessor http_client: _HTTPClient?
|
30
|
+
|
31
|
+
def initialize: () -> void
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_writer self.configuration: Configuration
|
35
|
+
|
36
|
+
def self.configuration: () -> Configuration
|
37
|
+
|
38
|
+
def self.configure: () { (Configuration) -> untyped } -> Configuration
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: atproto_auth
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Josh Huckabee
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: jose
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.2'
|
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.9'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.9'
|
41
|
+
description: A Ruby library for implementing AT Protocol OAuth flows, including DPoP,
|
42
|
+
PAR, and dynamic client registration. Supports both client and server-side implementations
|
43
|
+
with comprehensive security features.
|
44
|
+
email:
|
45
|
+
- mail@joshhuckabee.com
|
46
|
+
executables: []
|
47
|
+
extensions: []
|
48
|
+
extra_rdoc_files: []
|
49
|
+
files:
|
50
|
+
- ".rubocop.yml"
|
51
|
+
- CHANGELOG.md
|
52
|
+
- LICENSE.txt
|
53
|
+
- README.md
|
54
|
+
- Rakefile
|
55
|
+
- examples/confidential_client/Gemfile
|
56
|
+
- examples/confidential_client/Gemfile.lock
|
57
|
+
- examples/confidential_client/README.md
|
58
|
+
- examples/confidential_client/app.rb
|
59
|
+
- examples/confidential_client/config.ru
|
60
|
+
- examples/confidential_client/config/client-metadata.json
|
61
|
+
- examples/confidential_client/public/client-metadata.json
|
62
|
+
- examples/confidential_client/public/styles.css
|
63
|
+
- examples/confidential_client/scripts/generate_keys.rb
|
64
|
+
- examples/confidential_client/views/authorized.erb
|
65
|
+
- examples/confidential_client/views/index.erb
|
66
|
+
- examples/confidential_client/views/layout.erb
|
67
|
+
- lib/atproto_auth.rb
|
68
|
+
- lib/atproto_auth/client.rb
|
69
|
+
- lib/atproto_auth/client_metadata.rb
|
70
|
+
- lib/atproto_auth/configuration.rb
|
71
|
+
- lib/atproto_auth/dpop/client.rb
|
72
|
+
- lib/atproto_auth/dpop/key_manager.rb
|
73
|
+
- lib/atproto_auth/dpop/nonce_manager.rb
|
74
|
+
- lib/atproto_auth/dpop/proof_generator.rb
|
75
|
+
- lib/atproto_auth/errors.rb
|
76
|
+
- lib/atproto_auth/http_client.rb
|
77
|
+
- lib/atproto_auth/identity.rb
|
78
|
+
- lib/atproto_auth/identity/document.rb
|
79
|
+
- lib/atproto_auth/identity/resolver.rb
|
80
|
+
- lib/atproto_auth/par.rb
|
81
|
+
- lib/atproto_auth/par/client.rb
|
82
|
+
- lib/atproto_auth/par/client_assertion.rb
|
83
|
+
- lib/atproto_auth/par/request.rb
|
84
|
+
- lib/atproto_auth/par/response.rb
|
85
|
+
- lib/atproto_auth/pkce.rb
|
86
|
+
- lib/atproto_auth/server_metadata.rb
|
87
|
+
- lib/atproto_auth/server_metadata/authorization_server.rb
|
88
|
+
- lib/atproto_auth/server_metadata/origin_url.rb
|
89
|
+
- lib/atproto_auth/server_metadata/resource_server.rb
|
90
|
+
- lib/atproto_auth/state.rb
|
91
|
+
- lib/atproto_auth/state/session.rb
|
92
|
+
- lib/atproto_auth/state/session_manager.rb
|
93
|
+
- lib/atproto_auth/state/token_set.rb
|
94
|
+
- lib/atproto_auth/version.rb
|
95
|
+
- sig/atproto_auth.rbs
|
96
|
+
- sig/atproto_auth/client_metadata.rbs
|
97
|
+
- sig/atproto_auth/dpop/client.rbs
|
98
|
+
- sig/atproto_auth/dpop/key_manager.rbs
|
99
|
+
- sig/atproto_auth/dpop/nonce_manager.rbs
|
100
|
+
- sig/atproto_auth/dpop/proof_generator.rbs
|
101
|
+
- sig/atproto_auth/http_client.rbs
|
102
|
+
- sig/atproto_auth/identity/document.rbs
|
103
|
+
- sig/atproto_auth/identity/resolver.rbs
|
104
|
+
- sig/atproto_auth/par/client.rbs
|
105
|
+
- sig/atproto_auth/par/request.rbs
|
106
|
+
- sig/atproto_auth/par/response.rbs
|
107
|
+
- sig/atproto_auth/pkce.rbs
|
108
|
+
- sig/atproto_auth/server_metadata/authorization_server.rbs
|
109
|
+
- sig/atproto_auth/server_metadata/origin_url.rbs
|
110
|
+
- sig/atproto_auth/server_metadata/resource_server.rbs
|
111
|
+
- sig/atproto_auth/state/session.rbs
|
112
|
+
- sig/atproto_auth/state/session_manager.rbs
|
113
|
+
- sig/atproto_auth/state/token_set.rbs
|
114
|
+
- sig/atproto_auth/version.rbs
|
115
|
+
homepage: https://github.com/jhuckabee/atproto_auth
|
116
|
+
licenses:
|
117
|
+
- MIT
|
118
|
+
metadata:
|
119
|
+
homepage_uri: https://github.com/jhuckabee/atproto_auth
|
120
|
+
source_code_uri: https://github.com/jhuckabee/atproto_auth
|
121
|
+
changelog_uri: https://github.com/jhuckabee/atproto_auth/blob/main/CHANGELOG.md
|
122
|
+
rubygems_mfa_required: 'true'
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
require_paths:
|
126
|
+
- lib
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 3.0.0
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
requirements: []
|
138
|
+
rubygems_version: 3.5.23
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: Ruby implementation of the AT Protocol OAuth specification
|
142
|
+
test_files: []
|