cognito_idp_rails 1.0.0 → 1.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2e90ec3ceff76f7525a0cb71ab3f0457fef60d32cfe41fe5d4f27a3a2b0e916
4
- data.tar.gz: 62218081d1ebd666aa06280a31b2e2ae8a587a3abe5858b086bcabaebdc6aaf2
3
+ metadata.gz: cd19a4834a4a544ed409f858802b2587ceb41238b760000f99c64f225e8aa75a
4
+ data.tar.gz: 314efa955ed11cf93b4001acd66bbcd57e91310fc24999760c4ea4b844a43bff
5
5
  SHA512:
6
- metadata.gz: '0793b32cdb97929f1cc53a49a51b087efb6a9053e380e4aae97ba58b42310a86227fe87f82bd5e3a25d1e668499ad0643519d470cf24761ef6a12355adeff9c1'
7
- data.tar.gz: 34e1e1c5930521393a61c4ed1b04da405035d8c58b6f8da845db1c871a87237ded53b4d2fddd9a0dfc17d7dc4c912f107618f82925b12edefc42090f22daf534
6
+ metadata.gz: '0903452da2cff398668f3494f10ba811bed64cd34c90726da50bcaaa307107413d609aceb644c3301ca45912f43435c14e294d4d4e6b39465869a69588e9c286'
7
+ data.tar.gz: bd52e9bbc6bf2d313f9931d1bd63fddd9a0faf81e7f3c20aef0d173c9a07af5936e0d7964fea0b2624c6e7f3df1f381cf60bab38f53269f606e1d835e6e3cbff
data/README.md CHANGED
@@ -1,6 +1,12 @@
1
- # CognitoIdpRails
1
+ # CognitoIdpRails [![Ruby](https://github.com/appercept/cognito_idp_rails/actions/workflows/main.yml/badge.svg)](https://github.com/appercept/cognito_idp_rails/actions/workflows/main.yml)
2
2
 
3
- Simple integration of Amazon Cognito IdP (User Pools) for Rails applications.
3
+ Simple integration of Amazon Cognito IdP (User Pools) for Rails applications. Provides OAuth + PKCE authentication with sensible defaults and minimal configuration.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby >= 3.2.0
8
+ - Rails >= 7.0.0
9
+ - [cognito_idp](https://github.com/appercept/cognito_idp) ~> 1.0 (installed automatically)
4
10
 
5
11
  ## Installation
6
12
 
@@ -12,17 +18,100 @@ If bundler is not being used to manage dependencies, install the gem by executin
12
18
 
13
19
  $ gem install cognito_idp_rails
14
20
 
15
- ## Usage
21
+ ## Setup
16
22
 
17
- After adding the gem to your application, run the install generator:
23
+ Run the install generator:
18
24
 
19
25
  $ rails generate cognito_idp_rails:install
20
26
 
21
- This generator will add `cognito_idp` to your routes and install an initializer at `config/initializers/cognito_idp.rb`.
22
-
23
- Be sure to review and edit the initializer to configure options for your Amazon Cognito User Pool configuration. You
24
- must also provide an implementation for the `after_login` function in the initializer appropriate for any actions you
25
- want to take when a user signed in.
27
+ This will:
28
+
29
+ 1. Add a `cognito_idp` route entry to `config/routes.rb`
30
+ 2. Create an initializer at `config/initializers/cognito_idp.rb`
31
+
32
+ Review and edit the initializer to match your Amazon Cognito User Pool configuration.
33
+
34
+ ## Configuration
35
+
36
+ ```ruby
37
+ CognitoIdpRails.configure do |config|
38
+ config.domain = ENV["COGNITO_DOMAIN"]
39
+ config.client_id = ENV["COGNITO_CLIENT_ID"]
40
+ config.client_secret = ENV["COGNITO_CLIENT_SECRET"]
41
+
42
+ config.scope = "openid" # OAuth scope(s)
43
+ config.after_login_route = "/" # Redirect after login
44
+ config.after_logout_route = "/" # Redirect after logout
45
+
46
+ config.after_login = lambda do |token, user_info, request|
47
+ user = User.where(identifier: user_info.sub).first_or_create do |u|
48
+ u.email = user_info.email
49
+ end
50
+ request.session[:user_id] = user.id
51
+ end
52
+
53
+ config.before_logout = lambda do |request|
54
+ # Runs before the session is reset on logout
55
+ end
56
+
57
+ config.on_login_error = lambda do |error, request|
58
+ # Runs when token exchange or user info retrieval fails
59
+ Rails.logger.error("Login failed: #{error.message}")
60
+ end
61
+ end
62
+ ```
63
+
64
+ ### Options
65
+
66
+ | Option | Required | Default | Description |
67
+ |---|---|---|---|
68
+ | `domain` | Yes | — | Your Cognito User Pool domain (e.g. `your-app.auth.us-east-1.amazoncognito.com`) |
69
+ | `client_id` | Yes | — | The app client ID from your Cognito User Pool |
70
+ | `client_secret` | Yes | — | The app client secret from your Cognito User Pool |
71
+ | `scope` | No | `"openid"` | OAuth scope(s) to request |
72
+ | `after_login_route` | No | `"/"` | Path to redirect to after login |
73
+ | `after_logout_route` | No | `"/"` | Path to redirect to after logout |
74
+ | `after_login` | No | no-op | `lambda { \|token, user_info, request\| }` — called after successful authentication |
75
+ | `before_logout` | No | no-op | `lambda { \|request\| }` — called before session reset on logout |
76
+ | `on_login_error` | No | no-op | `lambda { \|error, request\| }` — called when login fails (receives a `CognitoIdp::Error`) |
77
+
78
+ ## Routes
79
+
80
+ The `cognito_idp` route helper adds four routes:
81
+
82
+ | Route | Controller Action | Purpose |
83
+ |---|---|---|
84
+ | `GET /login` | `sessions#login` | Redirects to Cognito authorization endpoint |
85
+ | `GET /auth/login_callback` | `sessions#login_callback` | Handles the OAuth callback from Cognito |
86
+ | `GET /logout` | `sessions#logout` | Redirects to Cognito logout endpoint |
87
+ | `GET /auth/logout_callback` | `sessions#logout_callback` | Handles the logout callback from Cognito |
88
+
89
+ ## How it works
90
+
91
+ CognitoIdpRails implements the OAuth 2.0 Authorization Code flow with PKCE (Proof Key for Code Exchange):
92
+
93
+ 1. User visits `/login`
94
+ 2. A PKCE code verifier/challenge pair and CSRF state token are generated and stored in the session
95
+ 3. User is redirected to your Cognito User Pool's authorization endpoint
96
+ 4. After authenticating with Cognito, the user is redirected back to `/auth/login_callback`
97
+ 5. The CSRF state is verified, and the authorization code is exchanged for tokens using the PKCE code verifier
98
+ 6. User info is fetched from Cognito using the access token
99
+ 7. The session is reset, the `after_login` callback is called, and the user is redirected to `after_login_route`
100
+
101
+ On logout, the user is redirected to Cognito's logout endpoint, then back to `/auth/logout_callback` where `before_logout` is called and the session is reset.
102
+
103
+ ## Customizing flash messages (i18n)
104
+
105
+ Flash messages are set via i18n. Override them in your application's locale files:
106
+
107
+ ```yaml
108
+ en:
109
+ cognito_idp_rails:
110
+ sessions:
111
+ login_success: "You have been successfully logged in."
112
+ login_failed: "Login failed."
113
+ logout_success: "You have been successfully logged out."
114
+ ```
26
115
 
27
116
  ## Development
28
117
 
@@ -9,15 +9,16 @@ module CognitoIdpRails
9
9
  end
10
10
 
11
11
  def login_callback
12
- client.get_token(grant_type: :authorization_code, code: params[:code], redirect_uri: auth_login_callback_url) do |token|
13
- client.get_user_info(token) do |user_info|
14
- reset_session
15
- configuration.after_login.call(token, user_info, request)
16
- redirect_to configuration.after_login_route, notice: "You have been successfully logged in."
17
- return
18
- end
19
- end
20
- redirect_to configuration.after_login_route, notice: "Login failed."
12
+ verifier = session.delete(:code_verifier)
13
+ session.delete(:login_state)
14
+ token = client.get_token(grant_type: :authorization_code, code: params[:code], redirect_uri: auth_login_callback_url, code_verifier: verifier)
15
+ user_info = client.get_user_info(token)
16
+ reset_session
17
+ configuration.after_login.call(token, user_info, request)
18
+ redirect_to configuration.after_login_route, notice: I18n.t("cognito_idp_rails.sessions.login_success")
19
+ rescue CognitoIdp::Error => e
20
+ configuration.on_login_error.call(e, request)
21
+ redirect_to configuration.after_login_route, notice: I18n.t("cognito_idp_rails.sessions.login_failed")
21
22
  end
22
23
 
23
24
  def logout
@@ -27,13 +28,19 @@ module CognitoIdpRails
27
28
  def logout_callback
28
29
  configuration.before_logout.call(request)
29
30
  reset_session
30
- redirect_to configuration.after_logout_route, notice: "You have been successfully logged out."
31
+ redirect_to configuration.after_logout_route, notice: I18n.t("cognito_idp_rails.sessions.logout_success")
31
32
  end
32
33
 
33
34
  private
34
35
 
35
36
  def authorization_url
36
- client.authorization_uri(redirect_uri: auth_login_callback_url, scope: scope, state: login_state)
37
+ client.authorization_uri(
38
+ redirect_uri: auth_login_callback_url,
39
+ scope: scope,
40
+ state: login_state,
41
+ code_challenge: code_challenge,
42
+ code_challenge_method: "S256"
43
+ )
37
44
  end
38
45
 
39
46
  def client
@@ -48,6 +55,14 @@ module CognitoIdpRails
48
55
  configuration.scope
49
56
  end
50
57
 
58
+ def code_verifier
59
+ session[:code_verifier] ||= SecureRandom.urlsafe_base64(32)
60
+ end
61
+
62
+ def code_challenge
63
+ Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
64
+ end
65
+
51
66
  def login_state
52
67
  session[:login_state] ||= SecureRandom.urlsafe_base64
53
68
  end
@@ -55,7 +70,7 @@ module CognitoIdpRails
55
70
  def verify_state
56
71
  return if params[:state] == login_state
57
72
 
58
- redirect_to configuration.after_login_route, notice: "Login failed."
73
+ redirect_to configuration.after_login_route, notice: I18n.t("cognito_idp_rails.sessions.login_failed")
59
74
  end
60
75
  end
61
76
  end
@@ -0,0 +1,6 @@
1
+ en:
2
+ cognito_idp_rails:
3
+ sessions:
4
+ login_success: "You have been successfully logged in."
5
+ login_failed: "Login failed."
6
+ logout_success: "You have been successfully logged out."
@@ -1,13 +1,24 @@
1
1
  module CognitoIdpRails
2
2
  class Configuration
3
3
  attr_accessor :after_login_route, :after_logout_route, :domain, :client_id,
4
- :client_secret, :after_login, :before_logout, :scope
4
+ :client_secret, :after_login, :before_logout, :on_login_error, :scope
5
+
6
+ REQUIRED_ATTRIBUTES = %i[domain client_id client_secret].freeze
5
7
 
6
8
  def initialize
7
9
  @after_login_route = "/"
8
10
  @after_logout_route = "/"
9
11
  @after_login = lambda { |token, user_info, request| }
10
12
  @before_logout = lambda { |request| }
13
+ @on_login_error = lambda { |error, request| }
14
+ @scope = "openid"
15
+ end
16
+
17
+ def validate!
18
+ missing = REQUIRED_ATTRIBUTES.select { |attr| public_send(attr).nil? }
19
+ return if missing.empty?
20
+
21
+ raise ConfigurationError, "Missing required configuration: #{missing.join(", ")}"
11
22
  end
12
23
  end
13
24
  end
@@ -1,3 +1,3 @@
1
1
  module CognitoIdpRails
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -3,6 +3,8 @@ require "cognito_idp_rails/version"
3
3
  require "cognito_idp"
4
4
 
5
5
  module CognitoIdpRails
6
+ class ConfigurationError < StandardError; end
7
+
6
8
  autoload :Configuration, "cognito_idp_rails/configuration"
7
9
 
8
10
  module Routing
@@ -11,11 +13,14 @@ module CognitoIdpRails
11
13
 
12
14
  class << self
13
15
  def client
14
- @client ||= CognitoIdp::Client.new(
15
- client_id: configuration.client_id,
16
- client_secret: configuration.client_secret,
17
- domain: configuration.domain
18
- )
16
+ @client ||= begin
17
+ configuration.validate!
18
+ CognitoIdp::Client.new(
19
+ client_id: configuration.client_id,
20
+ client_secret: configuration.client_secret,
21
+ domain: configuration.domain
22
+ )
23
+ end
19
24
  end
20
25
 
21
26
  def configuration
metadata CHANGED
@@ -1,29 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cognito_idp_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Hatherall
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2023-12-14 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: cognito_idp
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - ">="
16
+ - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: 0.1.1
18
+ version: '1.0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - ">="
23
+ - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: 0.1.1
25
+ version: '1.0'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: rails
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -56,8 +55,8 @@ files:
56
55
  - app/helpers/cognito_idp_rails/application_helper.rb
57
56
  - app/jobs/cognito_idp_rails/application_job.rb
58
57
  - app/mailers/cognito_idp_rails/application_mailer.rb
59
- - app/models/cognito_idp_rails/application_record.rb
60
58
  - app/views/layouts/cognito_idp_rails/application.html.erb
59
+ - config/locales/en.yml
61
60
  - config/routes.rb
62
61
  - lib/cognito_idp_rails.rb
63
62
  - lib/cognito_idp_rails/configuration.rb
@@ -74,7 +73,6 @@ metadata:
74
73
  homepage_uri: https://github.com/appercept/cognito_idp_rails
75
74
  source_code_uri: https://github.com/appercept/cognito_idp_rails
76
75
  changelog_uri: https://github.com/appercept/cognito_idp_rails/CHANGELOG.md
77
- post_install_message:
78
76
  rdoc_options: []
79
77
  require_paths:
80
78
  - lib
@@ -82,15 +80,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
82
80
  requirements:
83
81
  - - ">="
84
82
  - !ruby/object:Gem::Version
85
- version: 2.6.0
83
+ version: 3.2.0
86
84
  required_rubygems_version: !ruby/object:Gem::Requirement
87
85
  requirements:
88
86
  - - ">="
89
87
  - !ruby/object:Gem::Version
90
88
  version: '0'
91
89
  requirements: []
92
- rubygems_version: 3.4.10
93
- signing_key:
90
+ rubygems_version: 3.7.2
94
91
  specification_version: 4
95
92
  summary: Simple Rails integration for Amazon Cognito IdP (User Pools)
96
93
  test_files: []
@@ -1,5 +0,0 @@
1
- module CognitoIdpRails
2
- class ApplicationRecord < ActiveRecord::Base
3
- self.abstract_class = true
4
- end
5
- end