12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
# File 'lib/generators/clavis/user_method/user_method_generator.rb', line 12
def create_concern
directory_path = "app/models/concerns"
FileUtils.mkdir_p(directory_path) unless File.directory?(directory_path)
create_file "app/models/concerns/clavis_user_methods.rb", <<~RUBY
# frozen_string_literal: true
# This concern provides methods for finding or creating users from OAuth data
# It is intended to be included in your User model
module ClavisUserMethods
extend ActiveSupport::Concern
#{" "}
# Include the OauthAuthenticatable module to get helper methods
included do
include Clavis::Models::OauthAuthenticatable if defined?(Clavis::Models::OauthAuthenticatable)
#{" "}
# Add a temporary attribute to track OAuth authentication during user creation
# This can be used to conditionally skip password validation for OAuth users
attr_accessor :skip_password_validation
#{" "}
# IMPORTANT: If your User model uses has_secure_password, you need to handle
# password validation. Uncomment and modify ONE of these approaches:
#
# APPROACH 1: Skip password validation for OAuth users (recommended)
# validates :password, presence: true, length: { minimum: 8 },
# unless: -> { skip_password_validation }, on: :create
#
# APPROACH 2: Set a random secure password for OAuth users
# before_validation :set_random_password, if: -> { skip_password_validation && respond_to?(:password=) }
#
# APPROACH 3: Use validate: false for OAuth users (less recommended)
# See the #find_or_create_from_clavis method below
#{" "}
# For approach 2, add this method
# def set_random_password
# self.password = SecureRandom.hex(16)
# self.password_confirmation = password if respond_to?(:password_confirmation=)
# end
end
class_methods do
# Find or create a user from OAuth authentication
# This method is called by Clavis when authenticating via OAuth
def find_or_create_from_clavis(auth_hash)
# First try to find an existing identity
# For OpenID Connect providers like Google, we use the sub claim as the identifier
# For other providers, we use the uid
identity = if auth_hash[:id_token_claims]&.dig(:sub)
Clavis::OauthIdentity.find_by(
provider: auth_hash[:provider],
uid: auth_hash[:id_token_claims][:sub]
)
else
Clavis::OauthIdentity.find_by(
provider: auth_hash[:provider],
uid: auth_hash[:uid]
)
end
return identity.user if identity&.user
# Extract email from auth_hash (try various possible locations)
email = extract_email_from_auth_hash(auth_hash)
#{" "}
# Try to find existing user by email if available
user = find_by(email: email) if email.present?
#{" "}
# Create a new user if none found
if user.nil?
# Convert to HashWithIndifferentAccess for reliable key access
info = auth_hash[:info].with_indifferent_access if auth_hash[:info]
claims = auth_hash[:id_token_claims].with_indifferent_access if auth_hash[:id_token_claims]
#{" "}
user = new(
email: email
# Add other required fields for your User model here, for example:
##{" "}
# With HashWithIndifferentAccess, access is reliable regardless of key type:
# first_name: info&.dig(:given_name) || info&.dig(:first_name),
# last_name: info&.dig(:family_name) || info&.dig(:last_name),
# name: info&.dig(:name),
# username: info&.dig(:nickname),
# avatar_url: info&.dig(:picture) || info&.dig(:image),
# terms_accepted: true # for required boolean fields
)
#{" "}
# Mark this user as coming from OAuth to skip password validation
# This works with the validation conditionals defined above
user.skip_password_validation = true
#{" "}
# APPROACH 1 & 2: Use standard save with conditional validation
user.save!
#{" "}
# APPROACH 3: Bypass validations entirely - use with caution, and only if approaches 1 & 2 don't work
# If your User model has complex validations that are incompatible with OAuth users,
# you might need to bypass validations. Uncomment this if needed:
# user.save(validate: false)
end
#{" "}
# Create or update the OAuth identity
identity = Clavis::OauthIdentity.find_or_initialize_by(
provider: auth_hash[:provider],
uid: auth_hash[:id_token_claims]&.dig(:sub) || auth_hash[:uid]
)
identity.user = user
identity.auth_data = auth_hash[:info]
identity.token = auth_hash.dig(:credentials, :token)
identity.refresh_token = auth_hash.dig(:credentials, :refresh_token)
identity.expires_at = auth_hash.dig(:credentials, :expires_at) ? Time.at(auth_hash.dig(:credentials, :expires_at)) : nil
identity.save!
#{" "}
# Set the oauth_user flag if available
user.update(oauth_user: true) if user.respond_to?(:oauth_user=)
#{" "}
# Optional: Update any fields on the User model that you want to keep in sync
# user.update(
# avatar_url: auth_hash.dig(:info, :image),
# last_oauth_login_at: Time.current,
# last_oauth_provider: auth_hash[:provider]
# )
#{" "}
user
end
#{" "}
private
#{" "}
# Helper method to extract email from various locations in the auth hash
def extract_email_from_auth_hash(auth_hash)
return nil unless auth_hash
#{" "}
# Try to get email from various possible locations
if auth_hash[:info]&.with_indifferent_access
info = auth_hash[:info].with_indifferent_access
return info[:email] if info[:email].present?
end
#{" "}
if auth_hash[:id_token_claims]&.with_indifferent_access
claims = auth_hash[:id_token_claims].with_indifferent_access
return claims[:email] if claims[:email].present?
end
#{" "}
if auth_hash[:extra]&.dig(:raw_info)&.with_indifferent_access
raw_info = auth_hash[:extra][:raw_info].with_indifferent_access
return raw_info[:email] if raw_info[:email].present?
end
#{" "}
nil
end
end
end
RUBY
say_status :create, "Created ClavisUserMethods concern", :green
end
|