aven 0.0.1 → 0.0.2

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/app/components/aven/views/oauth/error/component.html.erb +44 -0
  4. data/app/components/aven/views/oauth/error/component.rb +30 -0
  5. data/app/components/aven/views/static/index/component.html.erb +4 -4
  6. data/app/components/aven/views/static/index/component.rb +11 -0
  7. data/app/controllers/aven/admin/base.rb +4 -4
  8. data/app/controllers/aven/application_controller.rb +22 -0
  9. data/app/controllers/aven/auth_controller.rb +6 -58
  10. data/app/controllers/aven/oauth/auth0_controller.rb +84 -0
  11. data/app/controllers/aven/oauth/base_controller.rb +183 -0
  12. data/app/controllers/aven/oauth/documentation/auth0.md +387 -0
  13. data/app/controllers/aven/oauth/documentation/entra_id.md +608 -0
  14. data/app/controllers/aven/oauth/documentation/github.md +329 -0
  15. data/app/controllers/aven/oauth/documentation/google.md +253 -0
  16. data/app/controllers/aven/oauth/entra_id_controller.rb +92 -0
  17. data/app/controllers/aven/oauth/github_controller.rb +91 -0
  18. data/app/controllers/aven/oauth/google_controller.rb +64 -0
  19. data/app/controllers/aven/workspaces_controller.rb +20 -0
  20. data/app/controllers/concerns/aven/authentication.rb +49 -0
  21. data/app/controllers/concerns/aven/controller_helpers.rb +38 -0
  22. data/app/helpers/aven/application_helper.rb +2 -6
  23. data/app/models/aven/app_record.rb +1 -1
  24. data/app/models/aven/app_record_schema.rb +0 -1
  25. data/app/models/aven/log.rb +0 -1
  26. data/app/models/aven/loggable.rb +2 -3
  27. data/app/models/aven/user.rb +0 -23
  28. data/app/models/aven/workspace.rb +49 -5
  29. data/app/models/aven/workspace_role.rb +0 -1
  30. data/app/models/aven/workspace_user.rb +0 -1
  31. data/app/models/aven/workspace_user_role.rb +0 -1
  32. data/config/routes.rb +22 -7
  33. data/db/migrate/{20251003090752_create_aven_users.rb → 20200101000001_create_aven_users.rb} +1 -1
  34. data/db/migrate/{20251004182010_create_aven_workspace_users.rb → 20200101000003_create_aven_workspace_users.rb} +1 -1
  35. data/db/migrate/{20251004182020_create_aven_workspace_roles.rb → 20200101000004_create_aven_workspace_roles.rb} +1 -1
  36. data/db/migrate/{20251004182030_create_aven_workspace_user_roles.rb → 20200101000005_create_aven_workspace_user_roles.rb} +1 -1
  37. data/db/migrate/{20251004190000_create_aven_logs.rb → 20200101000006_create_aven_logs.rb} +2 -3
  38. data/db/migrate/{20251004190100_create_aven_app_record_schemas.rb → 20200101000007_create_aven_app_record_schemas.rb} +0 -1
  39. data/db/migrate/{20251004190110_create_aven_app_records.rb → 20200101000008_create_aven_app_records.rb} +0 -1
  40. data/lib/aven/configuration.rb +26 -10
  41. data/lib/aven/engine.rb +15 -16
  42. data/lib/aven/model/tenant_model.rb +91 -0
  43. data/lib/aven/model.rb +6 -0
  44. data/lib/aven/version.rb +1 -1
  45. metadata +42 -69
  46. data/config/initializers/devise.rb +0 -43
  47. /data/db/migrate/{20251004182000_create_aven_workspaces.rb → 20200101000002_create_aven_workspaces.rb} +0 -0
  48. /data/lib/tasks/{sqema_tasks.rake → aven_tasks.rake} +0 -0
@@ -0,0 +1,329 @@
1
+ # GitHub OAuth Implementation Documentation
2
+
3
+ ## Overview
4
+
5
+ GitHub OAuth 2.0 integration for the Aven application. This implementation supports authentication using GitHub accounts.
6
+
7
+ **Controller**: `app/controllers/aven/oauth/github_controller.rb`
8
+ **Routes**: `config/routes.rb:22-24`
9
+
10
+ ---
11
+
12
+ ## Default Scopes
13
+
14
+ ```
15
+ user:email
16
+ ```
17
+
18
+ ### Scope Breakdown
19
+
20
+ | Scope | Purpose | Description |
21
+ |-------|---------|-------------|
22
+ | `user:email` | Read email addresses | Grants read-only access to user's email addresses (including private emails) |
23
+
24
+ **Note**: GitHub scopes don't require admin approval. Users consent individually.
25
+
26
+ ---
27
+
28
+ ## OAuth 2.0 Flow
29
+
30
+ ### 1. Authorization Request
31
+
32
+ **Endpoint**: `https://github.com/login/oauth/authorize`
33
+
34
+ **Parameters**:
35
+ ```ruby
36
+ {
37
+ client_id: "YOUR_CLIENT_ID",
38
+ redirect_uri: "https://yourdomain.com/oauth/github/callback",
39
+ scope: "user:email",
40
+ state: "RANDOM_STATE_TOKEN"
41
+ }
42
+ ```
43
+
44
+ ### 2. Token Exchange
45
+
46
+ **Endpoint**: `https://github.com/login/oauth/access_token`
47
+
48
+ **Parameters**:
49
+ ```ruby
50
+ {
51
+ client_id: "YOUR_CLIENT_ID",
52
+ client_secret: "YOUR_CLIENT_SECRET",
53
+ code: "AUTHORIZATION_CODE",
54
+ redirect_uri: "https://yourdomain.com/oauth/github/callback"
55
+ }
56
+ ```
57
+
58
+ **Headers**:
59
+ ```ruby
60
+ {
61
+ "Accept" => "application/json"
62
+ }
63
+ ```
64
+
65
+ ### 3. User Info Retrieval
66
+
67
+ **User Profile Endpoint**: `https://api.github.com/user`
68
+
69
+ **Response**:
70
+ ```json
71
+ {
72
+ "id": 12345678,
73
+ "login": "johndoe",
74
+ "name": "John Doe",
75
+ "email": "john@example.com",
76
+ "avatar_url": "https://avatars.githubusercontent.com/u/12345678",
77
+ "bio": "Developer",
78
+ "company": "Acme Inc"
79
+ }
80
+ ```
81
+
82
+ **User Emails Endpoint**: `https://api.github.com/user/emails`
83
+
84
+ Used when public email is not available.
85
+
86
+ **Response**:
87
+ ```json
88
+ [
89
+ {
90
+ "email": "john@example.com",
91
+ "primary": true,
92
+ "verified": true,
93
+ "visibility": "private"
94
+ }
95
+ ]
96
+ ```
97
+
98
+ **Implementation Logic**:
99
+ - First tries to get email from user profile
100
+ - If email is null/blank, fetches from `/user/emails` endpoint
101
+ - Uses the primary verified email
102
+
103
+ ---
104
+
105
+ ## Configuration
106
+
107
+ ### Application Configuration
108
+
109
+ ```ruby
110
+ config.oauth_providers = {
111
+ github: {
112
+ client_id: ENV['GITHUB_CLIENT_ID'],
113
+ client_secret: ENV['GITHUB_CLIENT_SECRET'],
114
+ # Optional configurations:
115
+ scope: "user:email read:user"
116
+ }
117
+ }
118
+ ```
119
+
120
+ ### Environment Variables
121
+
122
+ ```bash
123
+ GITHUB_CLIENT_ID=Iv1.a1b2c3d4e5f6g7h8
124
+ GITHUB_CLIENT_SECRET=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0
125
+ ```
126
+
127
+ ---
128
+
129
+ ## GitHub OAuth App Setup
130
+
131
+ ### 1. Create OAuth App
132
+
133
+ 1. Go to [GitHub Settings](https://github.com/settings/developers)
134
+ 2. Click **OAuth Apps** > **New OAuth App**
135
+ 3. Fill in:
136
+ - **Application name**: Your application name
137
+ - **Homepage URL**: `https://yourdomain.com`
138
+ - **Authorization callback URL**: `https://yourdomain.com/oauth/github/callback`
139
+ - **Application description**: (optional)
140
+ 4. Click **Register application**
141
+ 5. Copy the **Client ID**
142
+ 6. Click **Generate a new client secret**
143
+ 7. Copy the **Client Secret** (shown only once)
144
+
145
+ ### 2. Note Your Credentials
146
+
147
+ - **Client ID**: `Iv1.a1b2c3d4e5f6g7h8`
148
+ - **Client Secret**: `a1b2c3d4e5f6g7h8i9j0...`
149
+
150
+ ---
151
+
152
+ ## Routes
153
+
154
+ ```ruby
155
+ # GitHub OAuth
156
+ get "github", to: "github#create", as: :github
157
+ get "github/callback", to: "github#callback", as: :github_callback
158
+ ```
159
+
160
+ **Available Routes**:
161
+ - `GET /oauth/github` → Initiates OAuth flow
162
+ - `GET /oauth/github/callback` → Handles OAuth callback
163
+
164
+ **Named Routes**:
165
+ - `oauth_github_path` → `/oauth/github`
166
+ - `oauth_github_callback_path` → `/oauth/github/callback`
167
+
168
+ ---
169
+
170
+ ## Additional Scopes (Optional)
171
+
172
+ ### User Information
173
+
174
+ ```ruby
175
+ scope: "user:email read:user"
176
+ ```
177
+
178
+ - `read:user` - Read all user profile data
179
+ - `user` - Full access to user profile (read and write)
180
+
181
+ ### Repository Access
182
+
183
+ ```ruby
184
+ scope: "user:email repo"
185
+ ```
186
+
187
+ - `repo` - Full control of private repositories
188
+ - `public_repo` - Access to public repositories only
189
+
190
+ ### Organization Access
191
+
192
+ ```ruby
193
+ scope: "user:email read:org"
194
+ ```
195
+
196
+ - `read:org` - Read org and team membership
197
+ - `write:org` - Manage org and teams
198
+
199
+ ### Gist Access
200
+
201
+ ```ruby
202
+ scope: "user:email gist"
203
+ ```
204
+
205
+ - `gist` - Create and edit gists
206
+
207
+ ---
208
+
209
+ ## GitHub API Usage
210
+
211
+ After authentication, use the access token to call GitHub API v3:
212
+
213
+ ### Get User Repositories
214
+
215
+ ```ruby
216
+ GET https://api.github.com/user/repos
217
+ Authorization: Bearer {access_token}
218
+ Accept: application/vnd.github.v3+json
219
+ ```
220
+
221
+ ### Get User Organizations
222
+
223
+ ```ruby
224
+ GET https://api.github.com/user/orgs
225
+ Authorization: Bearer {access_token}
226
+ Accept: application/vnd.github.v3+json
227
+ ```
228
+
229
+ ### Get Authenticated User
230
+
231
+ ```ruby
232
+ GET https://api.github.com/user
233
+ Authorization: Bearer {access_token}
234
+ Accept: application/vnd.github.v3+json
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Official Documentation
240
+
241
+ 1. **GitHub OAuth Documentation**
242
+ https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
243
+
244
+ 2. **GitHub API Documentation**
245
+ https://docs.github.com/en/rest
246
+
247
+ 3. **GitHub OAuth Scopes**
248
+ https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps
249
+
250
+ 4. **GitHub REST API - Users**
251
+ https://docs.github.com/en/rest/users/users
252
+
253
+ ---
254
+
255
+ ## Implementation Details
256
+
257
+ ### Email Handling
258
+
259
+ The implementation has special logic for retrieving emails:
260
+
261
+ ```ruby
262
+ # Fetch user profile
263
+ user_data = github_api_request(USER_INFO_URL, access_token)
264
+
265
+ # Fetch primary email if not public
266
+ email = user_data[:email]
267
+ if email.blank?
268
+ emails_data = github_api_request(USER_EMAIL_URL, access_token)
269
+ primary_email = emails_data.find { |e| e[:primary] && e[:verified] }
270
+ email = primary_email[:email] if primary_email
271
+ end
272
+ ```
273
+
274
+ This ensures we always get an email, even if the user's GitHub email is private.
275
+
276
+ ### API Request Headers
277
+
278
+ GitHub requires specific headers:
279
+
280
+ ```ruby
281
+ request["Authorization"] = "Bearer #{access_token}"
282
+ request["Accept"] = "application/vnd.github.v3+json"
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Troubleshooting
288
+
289
+ ### "redirect_uri_mismatch"
290
+
291
+ **Solution**: Ensure the callback URL in your GitHub OAuth App settings exactly matches:
292
+ ```
293
+ https://yourdomain.com/oauth/github/callback
294
+ ```
295
+
296
+ ### "bad_verification_code"
297
+
298
+ **Solution**: The authorization code has already been used or expired. Ask user to try again.
299
+
300
+ ### No email returned
301
+
302
+ **Solution**: Ensure you're requesting the `user:email` scope. The implementation will fallback to the `/user/emails` endpoint.
303
+
304
+ ### API rate limiting
305
+
306
+ **Solution**: Authenticated requests have a limit of 5,000 requests per hour. For unauthenticated requests, it's 60 per hour.
307
+
308
+ ---
309
+
310
+ ## Security Considerations
311
+
312
+ ### Token Expiration
313
+
314
+ GitHub access tokens **do not expire** by default (unlike other OAuth providers). This means:
315
+ - Tokens remain valid until manually revoked
316
+ - No need for refresh token logic
317
+ - Important to handle token revocation properly
318
+
319
+ Users can revoke access at: https://github.com/settings/applications
320
+
321
+ ### Scope Permissions
322
+
323
+ Request only the minimum scopes needed:
324
+ - ✅ `user:email` - For authentication only
325
+ - ❌ `repo` - Don't request unless you need repository access
326
+
327
+ ---
328
+
329
+ **Last Updated**: October 15, 2025
@@ -0,0 +1,253 @@
1
+ # Google OAuth Implementation Documentation
2
+
3
+ ## Overview
4
+
5
+ Google OAuth 2.0 integration for the Aven application. This implementation supports authentication using Google accounts (both personal Gmail accounts and Google Workspace accounts).
6
+
7
+ **Controller**: `app/controllers/aven/oauth/google_controller.rb`
8
+ **Routes**: `config/routes.rb:18-20`
9
+
10
+ ---
11
+
12
+ ## Default Scopes
13
+
14
+ ```
15
+ openid email profile
16
+ ```
17
+
18
+ ### Scope Breakdown
19
+
20
+ | Scope | Purpose | Admin Consent Required |
21
+ |-------|---------|------------------------|
22
+ | `openid` | OpenID Connect authentication | No |
23
+ | `email` | Access user's email address | No |
24
+ | `profile` | Access user's basic profile (name, picture) | No |
25
+
26
+ **Note**: These are basic OAuth scopes that don't require admin approval. Users consent individually.
27
+
28
+ ---
29
+
30
+ ## OAuth 2.0 Flow
31
+
32
+ ### 1. Authorization Request
33
+
34
+ **Endpoint**: `https://accounts.google.com/o/oauth2/v2/auth`
35
+
36
+ **Parameters**:
37
+ ```ruby
38
+ {
39
+ client_id: "YOUR_CLIENT_ID",
40
+ redirect_uri: "https://yourdomain.com/oauth/google/callback",
41
+ response_type: "code",
42
+ scope: "openid email profile",
43
+ state: "RANDOM_STATE_TOKEN",
44
+ access_type: "offline", # Optional: for refresh tokens
45
+ prompt: "select_account" # Optional: force account selection
46
+ }
47
+ ```
48
+
49
+ ### 2. Token Exchange
50
+
51
+ **Endpoint**: `https://www.googleapis.com/oauth2/v4/token`
52
+
53
+ **Parameters**:
54
+ ```ruby
55
+ {
56
+ code: "AUTHORIZATION_CODE",
57
+ client_id: "YOUR_CLIENT_ID",
58
+ client_secret: "YOUR_CLIENT_SECRET",
59
+ redirect_uri: "https://yourdomain.com/oauth/google/callback",
60
+ grant_type: "authorization_code"
61
+ }
62
+ ```
63
+
64
+ ### 3. User Info Retrieval
65
+
66
+ **Endpoint**: `https://www.googleapis.com/oauth2/v3/userinfo`
67
+
68
+ **Response**:
69
+ ```json
70
+ {
71
+ "sub": "1234567890",
72
+ "email": "user@gmail.com",
73
+ "email_verified": true,
74
+ "name": "John Doe",
75
+ "picture": "https://lh3.googleusercontent.com/a/..."
76
+ }
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Configuration
82
+
83
+ ### Application Configuration
84
+
85
+ ```ruby
86
+ config.oauth_providers = {
87
+ google: {
88
+ client_id: ENV['GOOGLE_CLIENT_ID'],
89
+ client_secret: ENV['GOOGLE_CLIENT_SECRET'],
90
+ # Optional configurations:
91
+ scope: "openid email profile",
92
+ access_type: "offline", # Receive refresh tokens
93
+ prompt: "select_account" # Force account selection
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### Environment Variables
99
+
100
+ ```bash
101
+ GOOGLE_CLIENT_ID=123456789-abcdefghijklmnop.apps.googleusercontent.com
102
+ GOOGLE_CLIENT_SECRET=GOCSPX-your_client_secret
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Google Cloud Console Setup
108
+
109
+ ### 1. Create OAuth Client ID
110
+
111
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com)
112
+ 2. Select or create a project
113
+ 3. Navigate to **APIs & Services** > **Credentials**
114
+ 4. Click **Create Credentials** > **OAuth client ID**
115
+ 5. Select **Web application**
116
+ 6. Configure:
117
+ - **Name**: Your application name
118
+ - **Authorized JavaScript origins**: `https://yourdomain.com`
119
+ - **Authorized redirect URIs**: `https://yourdomain.com/oauth/google/callback`
120
+ 7. Click **Create**
121
+ 8. Copy the **Client ID** and **Client Secret**
122
+
123
+ ### 2. Configure OAuth Consent Screen
124
+
125
+ 1. Go to **APIs & Services** > **OAuth consent screen**
126
+ 2. Select **External** (for public apps) or **Internal** (for Google Workspace only)
127
+ 3. Fill in:
128
+ - **App name**: Your application name
129
+ - **User support email**: Your email
130
+ - **Developer contact information**: Your email
131
+ 4. Add scopes (optional for basic profile)
132
+ 5. Save and continue
133
+
134
+ ### 3. Note Your Credentials
135
+
136
+ - **Client ID**: `123456789-...apps.googleusercontent.com`
137
+ - **Client Secret**: `GOCSPX-...`
138
+
139
+ ---
140
+
141
+ ## Routes
142
+
143
+ ```ruby
144
+ # Google OAuth
145
+ get "google", to: "google#create", as: :google
146
+ get "google/callback", to: "google#callback", as: :google_callback
147
+ ```
148
+
149
+ **Available Routes**:
150
+ - `GET /oauth/google` → Initiates OAuth flow
151
+ - `GET /oauth/google/callback` → Handles OAuth callback
152
+
153
+ **Named Routes**:
154
+ - `oauth_google_path` → `/oauth/google`
155
+ - `oauth_google_callback_path` → `/oauth/google/callback`
156
+
157
+ ---
158
+
159
+ ## Additional Scopes (Optional)
160
+
161
+ If you need access to more Google services, add these scopes:
162
+
163
+ ### Gmail API
164
+
165
+ ```ruby
166
+ scope: "openid email profile https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/gmail.send"
167
+ ```
168
+
169
+ - `gmail.readonly` - Read emails
170
+ - `gmail.send` - Send emails
171
+ - `gmail.modify` - Read and modify emails
172
+
173
+ ### Google Calendar
174
+
175
+ ```ruby
176
+ scope: "openid email profile https://www.googleapis.com/auth/calendar.readonly"
177
+ ```
178
+
179
+ - `calendar.readonly` - Read calendar events
180
+ - `calendar` - Full calendar access
181
+
182
+ ### Google Drive
183
+
184
+ ```ruby
185
+ scope: "openid email profile https://www.googleapis.com/auth/drive.readonly"
186
+ ```
187
+
188
+ - `drive.readonly` - Read Drive files
189
+ - `drive.file` - Access files created by app
190
+ - `drive` - Full Drive access
191
+
192
+ **Important**: Extended scopes may require verification by Google if your app is public.
193
+
194
+ ---
195
+
196
+ ## Official Documentation
197
+
198
+ 1. **Google OAuth 2.0 Guide**
199
+ https://developers.google.com/identity/protocols/oauth2
200
+
201
+ 2. **Using OAuth 2.0 to Access Google APIs**
202
+ https://developers.google.com/identity/protocols/oauth2/web-server
203
+
204
+ 3. **Google API Scopes**
205
+ https://developers.google.com/identity/protocols/oauth2/scopes
206
+
207
+ 4. **OpenID Connect**
208
+ https://developers.google.com/identity/openid-connect/openid-connect
209
+
210
+ ---
211
+
212
+ ## Security Considerations
213
+
214
+ ### Refresh Tokens
215
+
216
+ Set `access_type: "offline"` to receive refresh tokens for long-term access:
217
+
218
+ ```ruby
219
+ access_type: oauth_config[:access_type] || "offline"
220
+ ```
221
+
222
+ ### Account Selection
223
+
224
+ Use `prompt: "select_account"` to force users to select which Google account to use:
225
+
226
+ ```ruby
227
+ prompt: oauth_config[:prompt] || "select_account"
228
+ ```
229
+
230
+ This is useful if users have multiple Google accounts.
231
+
232
+ ---
233
+
234
+ ## Troubleshooting
235
+
236
+ ### "redirect_uri_mismatch"
237
+
238
+ **Solution**: Ensure the redirect URI in Google Cloud Console exactly matches:
239
+ ```
240
+ https://yourdomain.com/oauth/google/callback
241
+ ```
242
+
243
+ ### "invalid_client"
244
+
245
+ **Solution**: Check that your Client ID and Client Secret are correct.
246
+
247
+ ### "access_denied"
248
+
249
+ **Solution**: User declined the consent screen. This is normal user behavior.
250
+
251
+ ---
252
+
253
+ **Last Updated**: October 15, 2025
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module Aven
7
+ module Oauth
8
+ class EntraIdController < BaseController
9
+ # Microsoft Entra ID (formerly Azure AD) OAuth endpoints
10
+ # Supports both single-tenant and multi-tenant configurations
11
+ #
12
+ # MINIMAL_SCOPE: Authentication only (no Graph API access)
13
+ # Use this for simple login without accessing user data beyond basic profile
14
+ MINIMAL_SCOPE = "openid email profile"
15
+
16
+ # DEFAULT_SCOPE: Includes Graph API access for contacts and email
17
+ DEFAULT_SCOPE = "openid email profile User.Read Contacts.Read Mail.Send Mail.Read"
18
+
19
+ protected
20
+
21
+ def authorization_url(state)
22
+ params = {
23
+ client_id: oauth_config[:client_id],
24
+ redirect_uri: callback_url,
25
+ response_type: "code",
26
+ scope: oauth_config[:scope] || DEFAULT_SCOPE,
27
+ state:,
28
+ response_mode: "query"
29
+ }
30
+
31
+ # Optionally add domain_hint for faster login
32
+ params[:domain_hint] = oauth_config[:domain_hint] if oauth_config[:domain_hint].present?
33
+
34
+ # Optionally add prompt parameter
35
+ params[:prompt] = oauth_config[:prompt] if oauth_config[:prompt].present?
36
+
37
+ "#{entra_authorization_url}?#{params.to_query}"
38
+ end
39
+
40
+ def exchange_code_for_token(code)
41
+ params = {
42
+ client_id: oauth_config[:client_id],
43
+ client_secret: oauth_config[:client_secret],
44
+ code:,
45
+ redirect_uri: callback_url,
46
+ grant_type: "authorization_code",
47
+ scope: oauth_config[:scope] || DEFAULT_SCOPE
48
+ }
49
+
50
+ oauth_request(URI(entra_token_url), params)
51
+ end
52
+
53
+ def fetch_user_info(access_token)
54
+ response = oauth_get_request(URI(entra_userinfo_url), access_token)
55
+
56
+ {
57
+ id: response[:id] || response[:sub],
58
+ email: response[:mail] || response[:userPrincipalName] || response[:email],
59
+ name: response[:displayName] || response[:name],
60
+ picture: nil # Microsoft Graph doesn't return picture in userinfo by default
61
+ }
62
+ end
63
+
64
+ private
65
+
66
+ def callback_url
67
+ aven.oauth_entra_id_callback_url(host: request.host, protocol: "https://")
68
+ end
69
+
70
+ def oauth_config
71
+ @oauth_config ||= Aven.configuration.oauth_providers[:entra_id] || raise("Microsoft Entra ID OAuth not configured")
72
+ end
73
+
74
+ def tenant_id
75
+ @tenant_id ||= oauth_config[:tenant_id] || "common"
76
+ end
77
+
78
+ def entra_authorization_url
79
+ "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/authorize"
80
+ end
81
+
82
+ def entra_token_url
83
+ "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token"
84
+ end
85
+
86
+ def entra_userinfo_url
87
+ # Using Microsoft Graph API for user info
88
+ "https://graph.microsoft.com/v1.0/me"
89
+ end
90
+ end
91
+ end
92
+ end