standard_id 0.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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/stylesheets/standard_id/application.css +15 -0
  6. data/app/controllers/concerns/standard_id/api_authentication.rb +29 -0
  7. data/app/controllers/concerns/standard_id/web_authentication.rb +51 -0
  8. data/app/controllers/standard_id/api/authorization_controller.rb +73 -0
  9. data/app/controllers/standard_id/api/base_controller.rb +61 -0
  10. data/app/controllers/standard_id/api/oauth/base_controller.rb +22 -0
  11. data/app/controllers/standard_id/api/oauth/tokens_controller.rb +44 -0
  12. data/app/controllers/standard_id/api/oidc/logout_controller.rb +50 -0
  13. data/app/controllers/standard_id/api/passwordless_controller.rb +38 -0
  14. data/app/controllers/standard_id/api/providers_controller.rb +175 -0
  15. data/app/controllers/standard_id/api/userinfo_controller.rb +36 -0
  16. data/app/controllers/standard_id/web/account_controller.rb +32 -0
  17. data/app/controllers/standard_id/web/auth/callback/providers_controller.rb +126 -0
  18. data/app/controllers/standard_id/web/base_controller.rb +14 -0
  19. data/app/controllers/standard_id/web/login_controller.rb +69 -0
  20. data/app/controllers/standard_id/web/logout_controller.rb +20 -0
  21. data/app/controllers/standard_id/web/reset_password/confirm_controller.rb +46 -0
  22. data/app/controllers/standard_id/web/reset_password/start_controller.rb +27 -0
  23. data/app/controllers/standard_id/web/sessions_controller.rb +26 -0
  24. data/app/controllers/standard_id/web/signup_controller.rb +83 -0
  25. data/app/forms/standard_id/web/reset_password_confirm_form.rb +37 -0
  26. data/app/forms/standard_id/web/reset_password_start_form.rb +38 -0
  27. data/app/forms/standard_id/web/signup_form.rb +65 -0
  28. data/app/helpers/standard_id/application_helper.rb +4 -0
  29. data/app/jobs/standard_id/application_job.rb +4 -0
  30. data/app/mailers/standard_id/application_mailer.rb +6 -0
  31. data/app/models/concerns/standard_id/account_associations.rb +14 -0
  32. data/app/models/concerns/standard_id/credentiable.rb +12 -0
  33. data/app/models/standard_id/application_record.rb +5 -0
  34. data/app/models/standard_id/authorization_code.rb +86 -0
  35. data/app/models/standard_id/browser_session.rb +27 -0
  36. data/app/models/standard_id/client_application.rb +143 -0
  37. data/app/models/standard_id/client_secret_credential.rb +63 -0
  38. data/app/models/standard_id/credential.rb +16 -0
  39. data/app/models/standard_id/device_session.rb +38 -0
  40. data/app/models/standard_id/email_identifier.rb +5 -0
  41. data/app/models/standard_id/identifier.rb +25 -0
  42. data/app/models/standard_id/password_credential.rb +24 -0
  43. data/app/models/standard_id/passwordless_challenge.rb +30 -0
  44. data/app/models/standard_id/phone_number_identifier.rb +5 -0
  45. data/app/models/standard_id/service_session.rb +44 -0
  46. data/app/models/standard_id/session.rb +54 -0
  47. data/app/models/standard_id/username_identifier.rb +5 -0
  48. data/app/views/standard_id/web/account/edit.html.erb +26 -0
  49. data/app/views/standard_id/web/account/show.html.erb +31 -0
  50. data/app/views/standard_id/web/login/show.html.erb +108 -0
  51. data/app/views/standard_id/web/reset_password/confirm/show.html.erb +27 -0
  52. data/app/views/standard_id/web/reset_password/start/show.html.erb +20 -0
  53. data/app/views/standard_id/web/sessions/index.html.erb +112 -0
  54. data/app/views/standard_id/web/signup/show.html.erb +96 -0
  55. data/config/initializers/generators.rb +9 -0
  56. data/config/initializers/migration_helpers.rb +32 -0
  57. data/config/routes/api.rb +24 -0
  58. data/config/routes/web.rb +26 -0
  59. data/db/migrate/20250830000000_create_standard_id_client_applications.rb +56 -0
  60. data/db/migrate/20250830171553_create_standard_id_password_credentials.rb +10 -0
  61. data/db/migrate/20250830232800_create_standard_id_identifiers.rb +17 -0
  62. data/db/migrate/20250831075703_create_standard_id_credentials.rb +10 -0
  63. data/db/migrate/20250831154635_create_standard_id_sessions.rb +43 -0
  64. data/db/migrate/20250901134520_create_standard_id_client_secret_credentials.rb +20 -0
  65. data/db/migrate/20250903063000_create_standard_id_authorization_codes.rb +46 -0
  66. data/db/migrate/20250903135906_create_standard_id_passwordless_challenges.rb +22 -0
  67. data/lib/generators/standard_id/install/install_generator.rb +14 -0
  68. data/lib/generators/standard_id/install/templates/standard_id.rb +11 -0
  69. data/lib/standard_id/api/authentication_guard.rb +20 -0
  70. data/lib/standard_id/api/session_manager.rb +39 -0
  71. data/lib/standard_id/api/token_manager.rb +50 -0
  72. data/lib/standard_id/api_engine.rb +7 -0
  73. data/lib/standard_id/config.rb +69 -0
  74. data/lib/standard_id/engine.rb +5 -0
  75. data/lib/standard_id/errors.rb +55 -0
  76. data/lib/standard_id/jwt_service.rb +50 -0
  77. data/lib/standard_id/oauth/authorization_code_authorization_flow.rb +47 -0
  78. data/lib/standard_id/oauth/authorization_code_flow.rb +53 -0
  79. data/lib/standard_id/oauth/authorization_flow.rb +91 -0
  80. data/lib/standard_id/oauth/base_request_flow.rb +43 -0
  81. data/lib/standard_id/oauth/client_credentials_flow.rb +38 -0
  82. data/lib/standard_id/oauth/implicit_authorization_flow.rb +79 -0
  83. data/lib/standard_id/oauth/password_flow.rb +70 -0
  84. data/lib/standard_id/oauth/passwordless_otp_flow.rb +87 -0
  85. data/lib/standard_id/oauth/refresh_token_flow.rb +61 -0
  86. data/lib/standard_id/oauth/subflows/base.rb +19 -0
  87. data/lib/standard_id/oauth/subflows/social_login_grant.rb +66 -0
  88. data/lib/standard_id/oauth/subflows/traditional_code_grant.rb +52 -0
  89. data/lib/standard_id/oauth/token_grant_flow.rb +107 -0
  90. data/lib/standard_id/passwordless/base_strategy.rb +67 -0
  91. data/lib/standard_id/passwordless/email_strategy.rb +27 -0
  92. data/lib/standard_id/passwordless/sms_strategy.rb +29 -0
  93. data/lib/standard_id/version.rb +3 -0
  94. data/lib/standard_id/web/authentication_guard.rb +23 -0
  95. data/lib/standard_id/web/session_manager.rb +71 -0
  96. data/lib/standard_id/web/token_manager.rb +30 -0
  97. data/lib/standard_id/web_engine.rb +7 -0
  98. data/lib/standard_id.rb +49 -0
  99. data/lib/tasks/standard_id_tasks.rake +4 -0
  100. metadata +186 -0
@@ -0,0 +1,108 @@
1
+ <% content_for :title, "Sign In" %>
2
+
3
+ <div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
4
+ <div class="sm:mx-auto sm:w-full sm:max-w-md">
5
+ <img src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company" class="mx-auto h-10 w-auto dark:hidden" />
6
+ <img src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company" class="mx-auto hidden h-10 w-auto dark:block" />
7
+ <h2 class="mt-6 text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">Sign in to your account</h2>
8
+ </div>
9
+
10
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
11
+ <div class="bg-white px-6 py-12 shadow sm:rounded-lg sm:px-12 dark:bg-gray-800/50 dark:shadow-none dark:outline dark:outline-1 dark:-outline-offset-1 dark:outline-white/10">
12
+
13
+ <% if flash[:alert].present? %>
14
+ <div class="mb-4 rounded-md bg-red-50 p-4 text-sm text-red-700 dark:bg-red-900/20 dark:text-red-300"><%= flash[:alert] %></div>
15
+ <% end %>
16
+ <% if flash[:notice].present? %>
17
+ <div class="mb-4 rounded-md bg-green-50 p-4 text-sm text-green-700 dark:bg-green-900/20 dark:text-green-300"><%= flash[:notice] %></div>
18
+ <% end %>
19
+
20
+ <%= form_with url: login_path, method: :post, local: true, html: { class: "space-y-6" } do |form| %>
21
+ <%= form.hidden_field :redirect_uri, value: @redirect_uri %>
22
+
23
+ <div>
24
+ <%= form.label :email, "Email address", class: "block text-sm/6 font-medium text-gray-900 dark:text-white" %>
25
+ <div class="mt-2">
26
+ <%= form.email_field "login[email]", required: true, autofocus: true, autocomplete: "email", class: "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500" %>
27
+ </div>
28
+ </div>
29
+
30
+ <div>
31
+ <%= form.label :password, "Password", class: "block text-sm/6 font-medium text-gray-900 dark:text-white" %>
32
+ <div class="mt-2">
33
+ <%= form.password_field "login[password]", required: true, autocomplete: "current-password", class: "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500" %>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="flex items-center justify-between">
38
+ <div class="flex gap-3">
39
+ <div class="flex h-6 shrink-0 items-center">
40
+ <div class="group grid size-4 grid-cols-1">
41
+ <%= form.check_box "login[remember_me]", id: "remember-me", class: "col-start-1 row-start-1 appearance-none rounded border border-gray-300 bg-white checked:border-indigo-600 checked:bg-indigo-600 indeterminate:border-indigo-600 indeterminate:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 dark:border-white/10 dark:bg-white/5 dark:checked:border-indigo-500 dark:checked:bg-indigo-500 dark:indeterminate:border-indigo-500 dark:indeterminate:bg-indigo-500 dark:focus-visible:outline-indigo-500 forced-colors:appearance-auto" %>
42
+ <svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-gray-950/25 dark:group-has-[:disabled]:stroke-white/25">
43
+ <path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:checked]:opacity-100" />
44
+ <path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-0 group-has-[:indeterminate]:opacity-100" />
45
+ </svg>
46
+ </div>
47
+ </div>
48
+ <label for="remember-me" class="block text-sm/6 text-gray-900 dark:text-white">Remember me</label>
49
+ </div>
50
+
51
+ <div class="text-sm/6">
52
+ <%= link_to "Forgot password?", reset_password_start_path, class: "font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300" %>
53
+ </div>
54
+ </div>
55
+
56
+ <div>
57
+ <%= form.submit "Sign in", class: "flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500" %>
58
+ </div>
59
+ <% end %>
60
+
61
+ <% if StandardId.config.google_client_id.present? || StandardId.config.apple_client_id.present? %>
62
+ <div>
63
+ <div class="mt-10 flex items-center gap-x-6">
64
+ <div class="w-full flex-1 border-t border-gray-200 dark:border-white/10"></div>
65
+ <p class="text-nowrap text-sm/6 font-medium text-gray-900 dark:text-white">Or continue with</p>
66
+ <div class="w-full flex-1 border-t border-gray-200 dark:border-white/10"></div>
67
+ </div>
68
+
69
+ <div class="mt-6 grid grid-cols-2 gap-4">
70
+ <% if StandardId.config.google_client_id.present? %>
71
+ <%= form_with url: login_path, method: :post, local: true do |form| %>
72
+ <%= form.hidden_field :connection, value: "google-oauth2" %>
73
+ <%= form.hidden_field :redirect_uri, value: @redirect_uri %>
74
+ <button type="submit" class="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent dark:bg-white/10 dark:text-white dark:shadow-none dark:ring-white/5 dark:hover:bg-white/20">
75
+ <svg viewBox="0 0 24 24" aria-hidden="true" class="h-5 w-5">
76
+ <path d="M12.0003 4.75C13.7703 4.75 15.3553 5.36002 16.6053 6.54998L20.0303 3.125C17.9502 1.19 15.2353 0 12.0003 0C7.31028 0 3.25527 2.69 1.28027 6.60998L5.27028 9.70498C6.21525 6.86002 8.87028 4.75 12.0003 4.75Z" fill="#EA4335" />
77
+ <path d="M23.49 12.275C23.49 11.49 23.415 10.73 23.3 10H12V14.51H18.47C18.18 15.99 17.34 17.25 16.08 18.1L19.945 21.1C22.2 19.01 23.49 15.92 23.49 12.275Z" fill="#4285F4" />
78
+ <path d="M5.26498 14.2949C5.02498 13.5699 4.88501 12.7999 4.88501 11.9999C4.88501 11.1999 5.01998 10.4299 5.26498 9.7049L1.275 6.60986C0.46 8.22986 0 10.0599 0 11.9999C0 13.9399 0.46 15.7699 1.28 17.3899L5.26498 14.2949Z" fill="#FBBC05" />
79
+ <path d="M12.0004 24.0001C15.2404 24.0001 17.9654 22.935 19.9454 21.095L16.0804 18.095C15.0054 18.82 13.6204 19.245 12.0004 19.245C8.8704 19.245 6.21537 17.135 5.2654 14.29L1.27539 17.385C3.25539 21.31 7.3104 24.0001 12.0004 24.0001Z" fill="#34A853" />
80
+ </svg>
81
+ <span class="text-sm/6 font-semibold">Google</span>
82
+ </button>
83
+ <% end %>
84
+ <% end %>
85
+
86
+ <% if StandardId.config.apple_client_id.present? %>
87
+ <%= form_with url: login_path, method: :post, local: true do |form| %>
88
+ <%= form.hidden_field :connection, value: "apple" %>
89
+ <%= form.hidden_field :redirect_uri, value: @redirect_uri %>
90
+ <button type="submit" class="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent dark:bg-white/10 dark:text-white dark:shadow-none dark:ring-white/5 dark:hover:bg-white/20">
91
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-5 fill-[#24292F] dark:fill-white">
92
+ <path d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd" fill-rule="evenodd" />
93
+ </svg>
94
+ <span class="text-sm/6 font-semibold">Apple</span>
95
+ </button>
96
+ <% end %>
97
+ <% end %>
98
+ </div>
99
+ </div>
100
+ <% end %>
101
+ </div>
102
+
103
+ <p class="mt-10 text-center text-sm/6 text-gray-500 dark:text-gray-400">
104
+ Not a member?
105
+ <%= link_to "Sign up", signup_path(redirect_uri: @redirect_uri), class: "font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300" %>
106
+ </p>
107
+ </div>
108
+ </div>
@@ -0,0 +1,27 @@
1
+ <% content_for :title, "Set New Password" %>
2
+
3
+ <div class="container">
4
+ <h1>Set New Password</h1>
5
+
6
+ <p>Enter your new password below.</p>
7
+
8
+ <%= form_with url: reset_password_confirm_path, method: :patch, local: true do |form| %>
9
+ <%= form.hidden_field :token, value: params[:token] %>
10
+
11
+ <div class="form-group">
12
+ <%= form.label :password, "New Password" %>
13
+ <%= form.password_field :password, required: true, autofocus: true, minlength: 8, placeholder: "Enter new password (minimum 8 characters)" %>
14
+ </div>
15
+
16
+ <div class="form-group">
17
+ <%= form.label :password_confirmation, "Confirm New Password" %>
18
+ <%= form.password_field :password_confirmation, required: true, minlength: 8, placeholder: "Confirm new password" %>
19
+ </div>
20
+
21
+ <%= form.submit "Update Password", class: "btn" %>
22
+ <% end %>
23
+
24
+ <div class="text-center mt-3">
25
+ <%= link_to "Back to Sign In", login_path %>
26
+ </div>
27
+ </div>
@@ -0,0 +1,20 @@
1
+ <% content_for :title, "Reset Password" %>
2
+
3
+ <div class="container">
4
+ <h1>Reset Password</h1>
5
+
6
+ <p>Enter your email address and we'll send you instructions to reset your password.</p>
7
+
8
+ <%= form_with url: reset_password_start_path, method: :post, local: true do |form| %>
9
+ <div class="form-group">
10
+ <%= form.label :email, "Email Address" %>
11
+ <%= form.email_field :email, required: true, autofocus: true, placeholder: "Enter your email address" %>
12
+ </div>
13
+
14
+ <%= form.submit "Send Reset Instructions", class: "btn" %>
15
+ <% end %>
16
+
17
+ <div class="text-center mt-3">
18
+ <%= link_to "Back to Sign In", login_path %>
19
+ </div>
20
+ </div>
@@ -0,0 +1,112 @@
1
+ <% content_for :title, "Sessions" %>
2
+
3
+ <div class="px-4 sm:px-6 lg:px-8">
4
+ <div class="sm:flex sm:items-center">
5
+ <div class="sm:flex-auto">
6
+ <h1 class="text-base font-semibold text-gray-900 dark:text-white">Active Sessions</h1>
7
+ <p class="mt-2 text-sm text-gray-700 dark:text-gray-300">Manage devices and browsers that are signed in to your account.</p>
8
+ </div>
9
+ <div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
10
+ <%= link_to "Back to Account", account_path, class: "block rounded-md border border-gray-300 px-3 py-2 text-center text-sm font-semibold text-gray-900 hover:bg-gray-50 dark:border-gray-600 dark:text-white dark:hover:bg-white/5" %>
11
+ </div>
12
+ </div>
13
+
14
+ <div class="mt-8 flow-root">
15
+ <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
16
+ <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
17
+ <% if @sessions.any? %>
18
+ <table class="relative min-w-full divide-y divide-gray-300 dark:divide-white/15">
19
+ <thead>
20
+ <tr>
21
+ <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0 dark:text-white">Session</th>
22
+ <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">Type</th>
23
+ <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">Agent</th>
24
+ <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">Last Used</th>
25
+ <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">Expires</th>
26
+ <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white">Status</th>
27
+ <th scope="col" class="py-3.5 pl-3 pr-4 sm:pr-0">
28
+ <span class="sr-only">Actions</span>
29
+ </th>
30
+ </tr>
31
+ </thead>
32
+ <tbody class="divide-y divide-gray-200 bg-white dark:divide-white/10 dark:bg-gray-900">
33
+ <% @sessions.each do |s| %>
34
+ <% is_current = (s == @current_session) %>
35
+ <% now = Time.current %>
36
+ <% expired = s.respond_to?(:expires_at) && s.expires_at.present? && s.expires_at < now %>
37
+ <% revoked = s.respond_to?(:revoked_at) && s.revoked_at.present? %>
38
+ <% active = !expired && !revoked %>
39
+ <% type_label = (s.type || s.class.name).demodulize.sub('Session','').presence || 'Session' %>
40
+ <% agent = if s.respond_to?(:user_agent) && s.user_agent.present?
41
+ s.user_agent
42
+ elsif s.respond_to?(:device_agent) && s.device_agent.present?
43
+ s.device_agent
44
+ else
45
+ '—'
46
+ end %>
47
+ <tr>
48
+ <td class="whitespace-nowrap py-5 pl-4 pr-3 text-sm sm:pl-0">
49
+ <div class="flex items-center">
50
+ <div class="size-11 shrink-0">
51
+ <div class="size-11 rounded-full bg-indigo-100 text-indigo-700 flex items-center justify-center text-xs font-semibold dark:bg-indigo-900/40 dark:text-indigo-300">ID</div>
52
+ </div>
53
+ <div class="ml-4">
54
+ <div class="font-medium text-gray-900 dark:text-white"><%= s.id %></div>
55
+ <div class="mt-1 text-gray-500 dark:text-gray-400">
56
+ Created <%= l(s.created_at, format: :short) rescue s.created_at.strftime("%b %d, %Y %H:%M") %>
57
+ <% if is_current %>
58
+ <span class="ml-2 inline-flex items-center rounded-md bg-indigo-50 px-2 py-0.5 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-600/20 dark:bg-indigo-900/30 dark:text-indigo-300 dark:ring-indigo-500/50">Current</span>
59
+ <% end %>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </td>
64
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 dark:text-gray-400">
65
+ <div class="text-gray-900 dark:text-white"><%= type_label %></div>
66
+ <% if s.respond_to?(:device_id) && s.device_id.present? %>
67
+ <div class="mt-1 text-gray-500 dark:text-gray-400">Device ID: <%= s.device_id %></div>
68
+ <% end %>
69
+ </td>
70
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 dark:text-gray-400">
71
+ <div class="text-gray-900 truncate max-w-[22rem] dark:text-white"><%= agent %></div>
72
+ <% if s.respond_to?(:ip_address) && s.ip_address.present? %>
73
+ <div class="mt-1 text-gray-500 dark:text-gray-400">IP: <%= s.ip_address %></div>
74
+ <% end %>
75
+ </td>
76
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900 dark:text-white">
77
+ <%= l(s.updated_at, format: :short) rescue s.updated_at.strftime("%b %d, %Y %H:%M") %>
78
+ </td>
79
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-900 dark:text-white">
80
+ <% if s.respond_to?(:expires_at) && s.expires_at.present? %>
81
+ <%= l(s.expires_at, format: :short) rescue s.expires_at.strftime("%b %d, %Y %H:%M") %>
82
+ <% else %>
83
+
84
+ <% end %>
85
+ </td>
86
+ <td class="whitespace-nowrap px-3 py-5 text-sm text-gray-500 dark:text-gray-400">
87
+ <% if revoked %>
88
+ <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/20 dark:bg-red-900/30 dark:text-red-400 dark:ring-red-500/50">Revoked</span>
89
+ <% elsif expired %>
90
+ <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-xs font-medium text-yellow-800 ring-1 ring-inset ring-yellow-600/20 dark:bg-yellow-900/30 dark:text-yellow-300 dark:ring-yellow-500/50">Expired</span>
91
+ <% else %>
92
+ <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20 dark:bg-green-900/30 dark:text-green-400 dark:ring-green-500/50">Active</span>
93
+ <% end %>
94
+ </td>
95
+ <td class="whitespace-nowrap py-5 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
96
+ <% if !is_current && active %>
97
+ <%= button_to "Revoke", session_path(s), method: :delete, data: { confirm: "Revoke this session?" }, class: "inline-flex items-center rounded-md bg-red-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 dark:bg-red-500 dark:hover:bg-red-400 dark:focus-visible:outline-red-500" %>
98
+ <% elsif is_current %>
99
+ <span class="text-xs text-gray-500 dark:text-gray-400">Use Sign Out to end current session</span>
100
+ <% end %>
101
+ </td>
102
+ </tr>
103
+ <% end %>
104
+ </tbody>
105
+ </table>
106
+ <% else %>
107
+ <div class="rounded-md p-6 text-sm text-gray-600 ring-1 ring-inset ring-gray-200 dark:text-gray-300 dark:ring-white/10">No active sessions.</div>
108
+ <% end %>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
@@ -0,0 +1,96 @@
1
+ <% content_for :title, "Sign Up" %>
2
+
3
+ <div class="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
4
+ <div class="sm:mx-auto sm:w-full sm:max-w-md">
5
+ <img src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company" class="mx-auto h-10 w-auto dark:hidden" />
6
+ <img src="https://tailwindcss.com/plus-assets/img/logos/mark.svg?color=indigo&shade=500" alt="Your Company" class="mx-auto hidden h-10 w-auto dark:block" />
7
+ <h2 class="mt-6 text-center text-2xl/9 font-bold tracking-tight text-gray-900 dark:text-white">Create your account</h2>
8
+ </div>
9
+
10
+ <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]">
11
+ <div class="bg-white px-6 py-12 shadow sm:rounded-lg sm:px-12 dark:bg-gray-800/50 dark:shadow-none dark:outline dark:outline-1 dark:-outline-offset-1 dark:outline-white/10">
12
+
13
+ <% if flash[:alert].present? %>
14
+ <div class="mb-4 rounded-md bg-red-50 p-4 text-sm text-red-700 dark:bg-red-900/20 dark:text-red-300"><%= flash[:alert] %></div>
15
+ <% end %>
16
+ <% if flash[:notice].present? %>
17
+ <div class="mb-4 rounded-md bg-green-50 p-4 text-sm text-green-700 dark:bg-green-900/20 dark:text-green-300"><%= flash[:notice] %></div>
18
+ <% end %>
19
+
20
+ <%= form_with url: signup_path, method: :post, local: true, html: { class: "space-y-6" } do |form| %>
21
+ <%= form.hidden_field :redirect_uri, value: @redirect_uri %>
22
+
23
+ <div>
24
+ <%= form.label :email, "Email address", class: "block text-sm/6 font-medium text-gray-900 dark:text-white" %>
25
+ <div class="mt-2">
26
+ <%= form.email_field "signup[email]", required: true, autofocus: true, autocomplete: "email", class: "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500" %>
27
+ </div>
28
+ </div>
29
+
30
+ <div>
31
+ <%= form.label :password, "Password", class: "block text-sm/6 font-medium text-gray-900 dark:text-white" %>
32
+ <div class="mt-2">
33
+ <%= form.password_field "signup[password]", required: true, autocomplete: "new-password", class: "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500" %>
34
+ </div>
35
+ </div>
36
+
37
+ <div>
38
+ <%= form.label :password_confirmation, "Confirm Password", class: "block text-sm/6 font-medium text-gray-900 dark:text-white" %>
39
+ <div class="mt-2">
40
+ <%= form.password_field "signup[password_confirmation]", required: true, autocomplete: "new-password", class: "block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500" %>
41
+ </div>
42
+ </div>
43
+
44
+ <div>
45
+ <%= form.submit "Create account", class: "flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm/6 font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500" %>
46
+ </div>
47
+ <% end %>
48
+
49
+ <% if StandardId.config.google_client_id.present? || StandardId.config.apple_client_id.present? %>
50
+ <div>
51
+ <div class="mt-10 flex items-center gap-x-6">
52
+ <div class="w-full flex-1 border-t border-gray-200 dark:border-white/10"></div>
53
+ <p class="text-nowrap text-sm/6 font-medium text-gray-900 dark:text-white">Or continue with</p>
54
+ <div class="w-full flex-1 border-t border-gray-200 dark:border-white/10"></div>
55
+ </div>
56
+
57
+ <div class="mt-6 grid grid-cols-2 gap-4">
58
+ <% if StandardId.config.google_client_id.present? %>
59
+ <%= form_with url: signup_path, method: :post, local: true do |form| %>
60
+ <%= form.hidden_field :connection, value: "google-oauth2" %>
61
+ <%= form.hidden_field :redirect_uri, value: @redirect_uri %>
62
+ <button type="submit" class="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent dark:bg-white/10 dark:text-white dark:shadow-none dark:ring-white/5 dark:hover:bg-white/20">
63
+ <svg viewBox="0 0 24 24" aria-hidden="true" class="h-5 w-5">
64
+ <path d="M12.0003 4.75C13.7703 4.75 15.3553 5.36002 16.6053 6.54998L20.0303 3.125C17.9502 1.19 15.2353 0 12.0003 0C7.31028 0 3.25527 2.69 1.28027 6.60998L5.27028 9.70498C6.21525 6.86002 8.87028 4.75 12.0003 4.75Z" fill="#EA4335" />
65
+ <path d="M23.49 12.275C23.49 11.49 23.415 10.73 23.3 10H12V14.51H18.47C18.18 15.99 17.34 17.25 16.08 18.1L19.945 21.1C22.2 19.01 23.49 15.92 23.49 12.275Z" fill="#4285F4" />
66
+ <path d="M5.26498 14.2949C5.02498 13.5699 4.88501 12.7999 4.88501 11.9999C4.88501 11.1999 5.01998 10.4299 5.26498 9.7049L1.275 6.60986C0.46 8.22986 0 10.0599 0 11.9999C0 13.9399 0.46 15.7699 1.28 17.3899L5.26498 14.2949Z" fill="#FBBC05" />
67
+ <path d="M12.0004 24.0001C15.2404 24.0001 17.9654 22.935 19.9454 21.095L16.0804 18.095C15.0054 18.82 13.6204 19.245 12.0004 19.245C8.8704 19.245 6.21537 17.135 5.2654 14.29L1.27539 17.385C3.25539 21.31 7.3104 24.0001 12.0004 24.0001Z" fill="#34A853" />
68
+ </svg>
69
+ <span class="text-sm/6 font-semibold">Google</span>
70
+ </button>
71
+ <% end %>
72
+ <% end %>
73
+
74
+ <% if StandardId.config.apple_client_id.present? %>
75
+ <%= form_with url: signup_path, method: :post, local: true do |form| %>
76
+ <%= form.hidden_field :connection, value: "apple" %>
77
+ <%= form.hidden_field :redirect_uri, value: @redirect_uri %>
78
+ <button type="submit" class="flex w-full items-center justify-center gap-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus-visible:ring-transparent dark:bg-white/10 dark:text-white dark:shadow-none dark:ring-white/5 dark:hover:bg-white/20">
79
+ <svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="size-5 fill-[#24292F] dark:fill-white">
80
+ <path d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd" fill-rule="evenodd" />
81
+ </svg>
82
+ <span class="text-sm/6 font-semibold">Apple</span>
83
+ </button>
84
+ <% end %>
85
+ <% end %>
86
+ </div>
87
+ </div>
88
+ <% end %>
89
+ </div>
90
+
91
+ <p class="mt-10 text-center text-sm/6 text-gray-500 dark:text-gray-400">
92
+ Already have an account?
93
+ <%= link_to "Sign in", login_path(redirect_uri: @redirect_uri), class: "font-semibold text-indigo-600 hover:text-indigo-500 dark:text-indigo-400 dark:hover:text-indigo-300" %>
94
+ </p>
95
+ </div>
96
+ </div>
@@ -0,0 +1,9 @@
1
+ StandardId::Engine.config.generators do |g|
2
+ g.test_framework :rspec,
3
+ fixtures: false,
4
+ view_specs: false,
5
+ helper_specs: false,
6
+ routing_specs: false,
7
+ controller_specs: false,
8
+ request_specs: true
9
+ end
@@ -0,0 +1,32 @@
1
+ # Monkey patch ActiveRecord::Migration to add primary_key_type and foreign_key_type methods
2
+ module ActiveRecord
3
+ class Migration
4
+ class << self
5
+ def primary_and_foreign_key_types
6
+ config = Rails.configuration.generators
7
+ config.options[config.orm][:primary_key_type] || :bigint
8
+ end
9
+
10
+ def primary_key_type
11
+ primary_and_foreign_key_types
12
+ end
13
+
14
+ def foreign_key_type
15
+ primary_and_foreign_key_types
16
+ end
17
+ end
18
+
19
+ # Make these methods available as instance methods too
20
+ def primary_and_foreign_key_types
21
+ self.class.primary_and_foreign_key_types
22
+ end
23
+
24
+ def primary_key_type
25
+ self.class.primary_key_type
26
+ end
27
+
28
+ def foreign_key_type
29
+ self.class.foreign_key_type
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ StandardId::ApiEngine.routes.draw do
2
+ scope module: :api do
3
+ resource :authorize, only: [:show], controller: :authorization
4
+
5
+ resource :userinfo, only: [:show], controller: :userinfo
6
+
7
+ resource :passwordless, only: [], controller: :passwordless do
8
+ post :start
9
+ end
10
+
11
+ namespace :oidc do
12
+ resource :logout, only: [:show], controller: :logout
13
+ end
14
+
15
+ namespace :oauth do
16
+ resource :token, only: [:create]
17
+
18
+ namespace :callback do
19
+ get :google, to: "providers#google"
20
+ post :apple, to: "providers#apple"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ StandardId::WebEngine.routes.draw do
2
+ scope module: :web do
3
+ # Authentication flows
4
+ resource :login, only: [:show, :create], controller: :login
5
+ resource :logout, only: [:create], controller: :logout
6
+ resource :signup, only: [:show, :create], controller: :signup
7
+
8
+ # Social authentication callbacks (web flow)
9
+ namespace :auth do
10
+ namespace :callback do
11
+ get :google, to: "providers#google"
12
+ post :apple, to: "providers#apple"
13
+ end
14
+ end
15
+
16
+ # Password reset
17
+ namespace :reset_password do
18
+ resource :start, only: [:show, :create], controller: :start
19
+ resource :confirm, only: [:show, :update], controller: :confirm
20
+ end
21
+
22
+ # Account management
23
+ resource :account, only: [:show, :edit, :update], controller: :account
24
+ resources :sessions, only: [:index, :destroy], controller: :sessions
25
+ end
26
+ end
@@ -0,0 +1,56 @@
1
+ class CreateStandardIdClientApplications < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :standard_id_client_applications do |t|
4
+ # Polymorphic owner association (Account, Organization, etc.)
5
+ t.references :owner, null: false, polymorphic: true, index: true
6
+
7
+ # Basic client information
8
+ t.string :name, null: false
9
+ t.text :description
10
+
11
+ # OAuth client identifier
12
+ t.string :client_id, null: false, index: { unique: true }
13
+
14
+ # OAuth configuration
15
+ t.text :redirect_uris, null: false
16
+ t.string :scopes, default: "openid profile email"
17
+ t.string :grant_types, default: "authorization_code refresh_token"
18
+ t.string :response_types, default: "code"
19
+
20
+ # PKCE configuration
21
+ t.boolean :require_pkce, null: false, default: true
22
+ t.string :code_challenge_methods, default: "S256"
23
+
24
+ # Token configuration
25
+ t.integer :access_token_lifetime, default: 3600 # 1 hour in seconds
26
+ t.integer :refresh_token_lifetime, default: 2592000 # 30 days in seconds
27
+ t.integer :authorization_code_lifetime, default: 600 # 10 minutes in seconds
28
+
29
+ # Client type and security
30
+ t.string :client_type, null: false, default: "confidential" # confidential or public
31
+ t.boolean :require_consent, null: false, default: true
32
+
33
+ # Lifecycle management
34
+ t.boolean :active, null: false, default: true
35
+ t.datetime :deactivated_at
36
+
37
+ # Metadata for extensibility
38
+ if connection.adapter_name.downcase.include?("postgres")
39
+ t.jsonb :metadata, default: {}, null: false
40
+ else
41
+ t.json :metadata, default: {}, null: false
42
+ end
43
+
44
+ t.timestamps
45
+
46
+ # Indexes
47
+ t.index [:owner_type, :owner_id]
48
+ t.index :active
49
+ t.index :client_type
50
+ end
51
+
52
+ if connection.adapter_name.downcase.include?("postgres")
53
+ add_index :standard_id_clients, :metadata, using: :gin
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,10 @@
1
+ class CreateStandardIdPasswordCredentials < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :standard_id_password_credentials, id: primary_key_type do |t|
4
+ t.string :login, null: false, index: { unique: true }
5
+ t.string :password_digest, null: false
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ class CreateStandardIdIdentifiers < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :standard_id_identifiers, id: primary_key_type do |t|
4
+ t.references :account, null: false, foreign_key: { to_table: StandardId.config.account_class.table_name }, index: true
5
+
6
+ t.string :type, null: false
7
+
8
+ t.string :value, null: false
9
+
10
+ t.timestamp :verified_at
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :standard_id_identifiers, [:account_id, :type, :value], unique: true
16
+ end
17
+ end
@@ -0,0 +1,10 @@
1
+ class CreateStandardIdCredentials < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :standard_id_credentials, id: primary_key_type do |t|
4
+ t.references :identifier, null: false, foreign_key: { to_table: :standard_id_identifiers }, index: true
5
+ t.references :credentialable, null: false, polymorphic: true, index: true
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,43 @@
1
+ class CreateStandardIdSessions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :standard_id_sessions, id: primary_key_type do |t|
4
+ t.references :account, null: false, foreign_key: true, index: true
5
+
6
+ # STI type column
7
+ t.string :type, null: false, index: true
8
+
9
+ # Base session columns
10
+ t.string :lookup_hash, null: false, index: { unique: true }
11
+ t.string :token_digest, null: false
12
+ t.string :ip_address
13
+ t.datetime :expires_at, null: false
14
+ t.datetime :revoked_at
15
+
16
+ if connection.adapter_name.downcase.include?("postgres")
17
+ t.jsonb :metadata, default: {}, null: false
18
+ t.index :metadata, using: :gin
19
+ else
20
+ t.json :metadata, default: {}, null: false
21
+ end
22
+
23
+ # BrowserSession columns
24
+ t.text :user_agent
25
+
26
+ # DeviceSession columns
27
+ t.string :device_id
28
+ t.text :device_agent
29
+ t.datetime :last_refreshed_at
30
+
31
+ # ServiceSession columns
32
+ t.references :owner, polymorphic: true, null: true, index: true
33
+ t.string :service_name
34
+ t.string :service_version
35
+
36
+ t.timestamps
37
+
38
+ t.index [:lookup_hash, :expires_at, :revoked_at]
39
+ t.index [:expires_at, :revoked_at]
40
+ t.index [:account_id, :type, :expires_at]
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ class CreateStandardIdClientSecretCredentials < ActiveRecord::Migration[7.1]
2
+ def change
3
+ create_table :standard_id_client_secret_credentials do |t|
4
+ t.string :name, null: false
5
+
6
+ t.references :client_application, null: false, foreign_key: { to_table: :standard_id_client_applications }, index: true
7
+
8
+ t.string :client_id, null: false, index: true # Denormalized for performance
9
+ t.string :client_secret_digest, null: false
10
+
11
+ t.string :scopes
12
+ t.string :redirect_uris
13
+
14
+ t.boolean :active, null: false, default: true
15
+ t.datetime :revoked_at
16
+
17
+ t.timestamps
18
+ end
19
+ end
20
+ end