better_auth 0.3.0 → 0.5.0

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +24 -0
  4. data/lib/better_auth/adapters/internal_adapter.rb +10 -7
  5. data/lib/better_auth/adapters/memory.rb +57 -11
  6. data/lib/better_auth/adapters/sql.rb +123 -20
  7. data/lib/better_auth/api.rb +114 -9
  8. data/lib/better_auth/async.rb +70 -0
  9. data/lib/better_auth/configuration.rb +97 -7
  10. data/lib/better_auth/context.rb +165 -12
  11. data/lib/better_auth/cookies.rb +6 -4
  12. data/lib/better_auth/core.rb +2 -0
  13. data/lib/better_auth/crypto/jwe.rb +27 -5
  14. data/lib/better_auth/crypto.rb +32 -0
  15. data/lib/better_auth/database_hooks.rb +8 -8
  16. data/lib/better_auth/deprecate.rb +28 -0
  17. data/lib/better_auth/endpoint.rb +92 -5
  18. data/lib/better_auth/error.rb +8 -1
  19. data/lib/better_auth/host.rb +166 -0
  20. data/lib/better_auth/instrumentation.rb +74 -0
  21. data/lib/better_auth/logger.rb +31 -0
  22. data/lib/better_auth/middleware/origin_check.rb +2 -2
  23. data/lib/better_auth/oauth2.rb +94 -0
  24. data/lib/better_auth/plugins/admin/schema.rb +2 -2
  25. data/lib/better_auth/plugins/admin.rb +344 -16
  26. data/lib/better_auth/plugins/anonymous.rb +37 -3
  27. data/lib/better_auth/plugins/device_authorization.rb +102 -5
  28. data/lib/better_auth/plugins/dub.rb +148 -0
  29. data/lib/better_auth/plugins/email_otp.rb +261 -19
  30. data/lib/better_auth/plugins/expo.rb +17 -1
  31. data/lib/better_auth/plugins/generic_oauth.rb +67 -35
  32. data/lib/better_auth/plugins/jwt.rb +37 -4
  33. data/lib/better_auth/plugins/last_login_method.rb +2 -2
  34. data/lib/better_auth/plugins/magic_link.rb +66 -3
  35. data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
  36. data/lib/better_auth/plugins/mcp/config.rb +51 -0
  37. data/lib/better_auth/plugins/mcp/consent.rb +31 -0
  38. data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
  39. data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
  40. data/lib/better_auth/plugins/mcp/registration.rb +31 -0
  41. data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
  42. data/lib/better_auth/plugins/mcp/schema.rb +91 -0
  43. data/lib/better_auth/plugins/mcp/token.rb +108 -0
  44. data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
  45. data/lib/better_auth/plugins/mcp.rb +111 -263
  46. data/lib/better_auth/plugins/multi_session.rb +61 -3
  47. data/lib/better_auth/plugins/oauth_protocol.rb +173 -30
  48. data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
  49. data/lib/better_auth/plugins/oidc_provider.rb +118 -14
  50. data/lib/better_auth/plugins/one_tap.rb +7 -2
  51. data/lib/better_auth/plugins/one_time_token.rb +42 -2
  52. data/lib/better_auth/plugins/open_api.rb +163 -318
  53. data/lib/better_auth/plugins/organization/schema.rb +6 -0
  54. data/lib/better_auth/plugins/organization.rb +186 -56
  55. data/lib/better_auth/plugins/phone_number.rb +141 -6
  56. data/lib/better_auth/plugins/siwe.rb +69 -3
  57. data/lib/better_auth/plugins/two_factor.rb +118 -41
  58. data/lib/better_auth/plugins/username.rb +57 -2
  59. data/lib/better_auth/rate_limiter.rb +38 -0
  60. data/lib/better_auth/request_state.rb +44 -0
  61. data/lib/better_auth/response.rb +42 -0
  62. data/lib/better_auth/router.rb +7 -1
  63. data/lib/better_auth/routes/account.rb +220 -42
  64. data/lib/better_auth/routes/email_verification.rb +98 -14
  65. data/lib/better_auth/routes/password.rb +126 -8
  66. data/lib/better_auth/routes/session.rb +128 -13
  67. data/lib/better_auth/routes/sign_in.rb +26 -2
  68. data/lib/better_auth/routes/sign_out.rb +13 -1
  69. data/lib/better_auth/routes/sign_up.rb +70 -4
  70. data/lib/better_auth/routes/social.rb +132 -7
  71. data/lib/better_auth/routes/user.rb +228 -20
  72. data/lib/better_auth/routes/validation.rb +50 -0
  73. data/lib/better_auth/secret_config.rb +115 -0
  74. data/lib/better_auth/session.rb +13 -2
  75. data/lib/better_auth/url_helpers.rb +206 -0
  76. data/lib/better_auth/version.rb +1 -1
  77. data/lib/better_auth.rb +12 -0
  78. metadata +23 -1
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Plugins
5
+ module MCP
6
+ module_function
7
+
8
+ def register_client(ctx, config)
9
+ set_cors_headers(ctx)
10
+ body = OAuthProtocol.stringify_keys(ctx.body)
11
+ body["token_endpoint_auth_method"] ||= "none"
12
+ body["grant_types"] ||= [OAuthProtocol::AUTH_CODE_GRANT, OAuthProtocol::REFRESH_GRANT]
13
+ body["response_types"] ||= ["code"]
14
+ body["require_pkce"] = true unless body.key?("require_pkce") || body.key?("requirePKCE")
15
+
16
+ OAuthProtocol.create_client(
17
+ ctx,
18
+ model: "oauthClient",
19
+ body: body,
20
+ default_auth_method: "none",
21
+ store_client_secret: config[:store_client_secret],
22
+ default_scopes: config[:scopes],
23
+ allowed_scopes: config[:scopes],
24
+ prefix: config[:prefix],
25
+ dynamic_registration: true,
26
+ strip_client_metadata: true
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Plugins
5
+ module MCP
6
+ module ResourceHandler
7
+ module_function
8
+
9
+ def with_mcp_auth(app, resource_metadata_url:, auth: nil, resource_metadata_mappings: {})
10
+ lambda do |env|
11
+ authorization = env["HTTP_AUTHORIZATION"].to_s
12
+ return unauthorized(resource_metadata_url) unless authorization.start_with?("Bearer ")
13
+
14
+ session = auth&.api&.get_mcp_session(headers: {"authorization" => authorization})
15
+ return unauthorized(resource_metadata_url) unless session
16
+
17
+ env["better_auth.mcp_session"] = session
18
+ app.call(env)
19
+ rescue APIError
20
+ unauthorized(resource_metadata_url)
21
+ end
22
+ end
23
+
24
+ def unauthorized(resource_metadata_url)
25
+ [
26
+ 401,
27
+ {
28
+ "www-authenticate" => %(Bearer resource_metadata="#{resource_metadata_url}"),
29
+ "access-control-expose-headers" => "WWW-Authenticate"
30
+ },
31
+ ["unauthorized"]
32
+ ]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Plugins
5
+ module MCP
6
+ module_function
7
+
8
+ def schema
9
+ {
10
+ oauthClient: {
11
+ modelName: "oauthClient",
12
+ fields: {
13
+ clientId: {type: "string", unique: true, required: true},
14
+ clientSecret: {type: "string", required: false},
15
+ disabled: {type: "boolean", default_value: false, required: false},
16
+ skipConsent: {type: "boolean", required: false},
17
+ enableEndSession: {type: "boolean", required: false},
18
+ clientSecretExpiresAt: {type: "number", required: false},
19
+ scopes: {type: "string[]", required: false},
20
+ userId: {type: "string", required: false},
21
+ createdAt: {type: "date", required: true, default_value: -> { Time.now }},
22
+ updatedAt: {type: "date", required: true, default_value: -> { Time.now }, on_update: -> { Time.now }},
23
+ name: {type: "string", required: false},
24
+ uri: {type: "string", required: false},
25
+ icon: {type: "string", required: false},
26
+ contacts: {type: "string[]", required: false},
27
+ tos: {type: "string", required: false},
28
+ policy: {type: "string", required: false},
29
+ softwareId: {type: "string", required: false},
30
+ softwareVersion: {type: "string", required: false},
31
+ softwareStatement: {type: "string", required: false},
32
+ redirectUris: {type: "string[]", required: true},
33
+ postLogoutRedirectUris: {type: "string[]", required: false},
34
+ tokenEndpointAuthMethod: {type: "string", required: false},
35
+ grantTypes: {type: "string[]", required: false},
36
+ responseTypes: {type: "string[]", required: false},
37
+ public: {type: "boolean", required: false},
38
+ type: {type: "string", required: false},
39
+ requirePKCE: {type: "boolean", required: false},
40
+ subjectType: {type: "string", required: false},
41
+ referenceId: {type: "string", required: false},
42
+ metadata: {type: "json", required: false}
43
+ }
44
+ },
45
+ oauthRefreshToken: {
46
+ fields: {
47
+ token: {type: "string", required: true},
48
+ clientId: {type: "string", required: true},
49
+ sessionId: {type: "string", required: false},
50
+ userId: {type: "string", required: false},
51
+ referenceId: {type: "string", required: false},
52
+ authTime: {type: "date", required: false},
53
+ expiresAt: {type: "date", required: false},
54
+ createdAt: {type: "date", required: true, default_value: -> { Time.now }},
55
+ revoked: {type: "date", required: false},
56
+ scopes: {type: "string[]", required: true}
57
+ }
58
+ },
59
+ oauthAccessToken: {
60
+ modelName: "oauthAccessToken",
61
+ fields: {
62
+ token: {type: "string", unique: true, required: true},
63
+ expiresAt: {type: "date", required: true},
64
+ clientId: {type: "string", required: true},
65
+ userId: {type: "string", required: false},
66
+ sessionId: {type: "string", required: false},
67
+ scopes: {type: "string[]", required: true},
68
+ revoked: {type: "date", required: false},
69
+ referenceId: {type: "string", required: false},
70
+ authTime: {type: "date", required: false},
71
+ refreshId: {type: "string", required: false},
72
+ createdAt: {type: "date", required: true, default_value: -> { Time.now }},
73
+ updatedAt: {type: "date", required: true, default_value: -> { Time.now }, on_update: -> { Time.now }}
74
+ }
75
+ },
76
+ oauthConsent: {
77
+ modelName: "oauthConsent",
78
+ fields: {
79
+ clientId: {type: "string", required: true},
80
+ userId: {type: "string", required: false},
81
+ referenceId: {type: "string", required: false},
82
+ scopes: {type: "string[]", required: true},
83
+ createdAt: {type: "date", required: true, default_value: -> { Time.now }},
84
+ updatedAt: {type: "date", required: true, default_value: -> { Time.now }, on_update: -> { Time.now }}
85
+ }
86
+ }
87
+ }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Plugins
5
+ module MCP
6
+ module_function
7
+
8
+ def token(ctx, config)
9
+ set_cors_headers(ctx)
10
+ body = OAuthProtocol.stringify_keys(ctx.body)
11
+ client = OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix])
12
+ audience = validate_resource!(config, body)
13
+
14
+ case body["grant_type"]
15
+ when OAuthProtocol::AUTH_CODE_GRANT
16
+ code = OAuthProtocol.consume_code!(
17
+ config[:store],
18
+ body["code"],
19
+ client_id: OAuthProtocol.stringify_keys(client)["clientId"],
20
+ redirect_uri: body["redirect_uri"],
21
+ code_verifier: body["code_verifier"]
22
+ )
23
+ OAuthProtocol.issue_tokens(
24
+ ctx,
25
+ config[:store],
26
+ model: "oauthAccessToken",
27
+ client: client,
28
+ session: code[:session],
29
+ scopes: code[:scopes],
30
+ include_refresh: code[:scopes].include?("offline_access"),
31
+ issuer: validate_issuer_url(OAuthProtocol.issuer(ctx)),
32
+ prefix: config[:prefix],
33
+ refresh_token_expires_in: config[:refresh_token_expires_in],
34
+ access_token_expires_in: config[:access_token_expires_in],
35
+ audience: audience,
36
+ grant_type: OAuthProtocol::AUTH_CODE_GRANT,
37
+ jwt_access_token: !audience.nil?,
38
+ nonce: code[:nonce],
39
+ auth_time: code[:auth_time],
40
+ reference_id: code[:reference_id],
41
+ filter_id_token_claims_by_scope: true
42
+ )
43
+ when OAuthProtocol::REFRESH_GRANT
44
+ OAuthProtocol.refresh_tokens(
45
+ ctx,
46
+ config[:store],
47
+ model: "oauthAccessToken",
48
+ client: client,
49
+ refresh_token: body["refresh_token"],
50
+ scopes: body["scope"],
51
+ issuer: validate_issuer_url(OAuthProtocol.issuer(ctx)),
52
+ prefix: config[:prefix],
53
+ refresh_token_expires_in: config[:refresh_token_expires_in],
54
+ access_token_expires_in: config[:access_token_expires_in],
55
+ audience: audience,
56
+ jwt_access_token: !audience.nil?,
57
+ filter_id_token_claims_by_scope: true
58
+ )
59
+ else
60
+ raise APIError.new("BAD_REQUEST", message: "unsupported_grant_type")
61
+ end
62
+ end
63
+
64
+ def validate_resource!(config, body)
65
+ resources = Array(body["resource"]).compact.map(&:to_s)
66
+ return nil if resources.empty?
67
+
68
+ valid = Array(config[:valid_audiences]).map(&:to_s)
69
+ resources.each do |resource|
70
+ raise APIError.new("BAD_REQUEST", message: "requested resource invalid") unless valid.empty? || valid.include?(resource)
71
+ end
72
+ (resources.length == 1) ? resources.first : resources
73
+ end
74
+
75
+ def introspect(ctx, config)
76
+ OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix])
77
+ body = OAuthProtocol.stringify_keys(ctx.body)
78
+ token_record = OAuthProtocol.find_token_by_hint(config[:store], body["token"].to_s, body["token_type_hint"], prefix: config[:prefix])
79
+ return inactive_token_response if token_record.nil? || token_record["revoked"] || (token_record["expiresAt"] && token_record["expiresAt"] <= Time.now)
80
+
81
+ {
82
+ active: true,
83
+ client_id: token_record["clientId"],
84
+ scope: OAuthProtocol.scope_string(token_record["scope"] || token_record["scopes"]),
85
+ sub: token_record["subject"] || token_record.dig("user", "id"),
86
+ iss: token_record["issuer"],
87
+ iat: token_record["issuedAt"]&.to_i,
88
+ exp: token_record["expiresAt"]&.to_i,
89
+ sid: token_record["sessionId"],
90
+ aud: token_record["audience"]
91
+ }.compact
92
+ end
93
+
94
+ def revoke(ctx, config)
95
+ OAuthProtocol.authenticate_client!(ctx, "oauthClient", store_client_secret: config[:store_client_secret], prefix: config[:prefix])
96
+ body = OAuthProtocol.stringify_keys(ctx.body)
97
+ if (token_record = OAuthProtocol.find_token_by_hint(config[:store], body["token"].to_s, body["token_type_hint"], prefix: config[:prefix]))
98
+ token_record["revoked"] = Time.now
99
+ end
100
+ {revoked: true}
101
+ end
102
+
103
+ def inactive_token_response
104
+ {active: false}
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+
5
+ module BetterAuth
6
+ module Plugins
7
+ module MCP
8
+ module_function
9
+
10
+ def userinfo(ctx, config)
11
+ OAuthProtocol.userinfo(config[:store], ctx.headers["authorization"], prefix: config[:prefix], jwt_secret: ctx.context.secret)
12
+ end
13
+
14
+ def session_from_token(ctx, config)
15
+ authorization = ctx.headers["authorization"].to_s
16
+ token_value = authorization.start_with?("Bearer ") ? authorization.delete_prefix("Bearer ").strip : authorization.strip
17
+ return nil if token_value.empty?
18
+
19
+ token_record = OAuthProtocol.token_record(config[:store], token_value, prefix: config[:prefix])
20
+ return token_record if token_record
21
+
22
+ payload = ::JWT.decode(token_value, ctx.context.secret, true, algorithm: "HS256").first
23
+ {
24
+ "clientId" => payload["azp"],
25
+ "userId" => payload["sub"],
26
+ "sessionId" => payload["sid"],
27
+ "scopes" => OAuthProtocol.parse_scopes(payload["scope"]),
28
+ "audience" => payload["aud"],
29
+ "subject" => payload["sub"],
30
+ "expiresAt" => payload["exp"] ? Time.at(payload["exp"].to_i) : nil
31
+ }.compact
32
+ rescue ::JWT::DecodeError
33
+ nil
34
+ end
35
+ end
36
+ end
37
+ end