<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Jcmonseu</id>
	<title>Expertiza_Wiki - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.expertiza.ncsu.edu/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Jcmonseu"/>
	<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Special:Contributions/Jcmonseu"/>
	<updated>2026-07-14T03:52:01Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=168134</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=168134"/>
		<updated>2026-04-27T23:59:50Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
&lt;br /&gt;
Expertiza currently authenticates users with its own login page, implemented by the Expertiza application. Expertiza has been used at many campuses, however, and each has their own SSO (single signon) protocol that students and staff use to log into other applications. Supporting these standard protocols at sites where they are in use is more secure for the application, provides a familiar and streamlined login experience, and frees Expertiza from managing credentials for users whose institution already does so. This design introduces [https://openid.net/developers/how-connect-works/ OIDC] login as an additional authentication option alongside the existing username and password login. Both methods will continue to be supported, allowing users to choose their preferred approach.&lt;br /&gt;
&lt;br /&gt;
== Design Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users enter their Expertiza username on the login page and select a provider from a dropdown. The frontend posts the username and provider to the backend, which returns an authorization URL. The user is redirected to the school's OIDC provider, authenticates, and is redirected back to the frontend callback. The callback posts the authorization code and state to the backend to complete login. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Matching ===&lt;br /&gt;
Match the authenticated user by both the Expertiza username (provided before login) and the verified email claim from the ID token. Username is required because email addresses are not unique in Expertiza; multiple accounts may share the same email. Matching is case-insensitive on both fields. The provider must validate email with the &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim and if it is not &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the login is rejected. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, a generic authentication error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
* OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Client credentials (client ID, client secret) are stored in environment variables and injected via ERB.&lt;br /&gt;
* Providers must support OIDC discovery;&lt;br /&gt;
** Their endpoints and JWKS keys are fetched automatically from the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document.&lt;br /&gt;
* The system supports multiple OIDC provider configurations simultaneously.&lt;br /&gt;
* Provider configuration is validated at boot via config/initializers/oidc.rb. In production, a provider with any missing required key raises OidcConfig::InvalidConfiguration, which prevents the application from starting with a misconfigured OIDC provider. In all other environments (development, test), the invalid provider is skipped with a warning logged so that local development and CI are not blocked when OIDC credentials are not configured.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, PKCE code verifier, username, and provider key are stored server-side in an &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and consumed (deleted) on successful callback. A probabilistic inline cleanup removes stale rows on new request creation to keep the table bounded without requiring a scheduled job. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Login ===&lt;br /&gt;
At one point it was suggested that OIDC completely replace password login if configured.  We considered this idea, however, given this feature must support multiple institutions, some of which may want OIDC and others which may not and there is no institution context pre-login at this time, we felt that probably falls under account management and is beyond the scope of this assignment.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted. The OIDC flow is only used to verify the user's identity with an external provider at login time. Once the user is authenticated, Expertiza issues its own session JWT, and all subsequent requests use that local session. The IdP session is independent of the Expertiza session, so logging out of Expertiza (destroying the local session) does not affect the user's session at the IdP, and logging out of the IdP does not affect the user's Expertiza session. RP-initiated logout (ending the IdP session as part of application logout) is out of scope.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
All callback failure modes (invalid state, expired state, replayed state, no matching user, mismatched username or email, failed token verification, unverified email, unknown provider) return a generic &amp;quot;Authentication failed&amp;quot; response with HTTP 401 to avoid leaking information about which specific check failed. Provider communication failures (discovery or token endpoint unreachable) return HTTP 502. Missing required parameters return HTTP 400. Unknown providers on &amp;lt;code&amp;gt;client-select&amp;lt;/code&amp;gt; return HTTP 404.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. State rows are atomically consumed in a database transaction with row-level locking to prevent race conditions on replay. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE. The &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim is required and must be true.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request and model specs stub the identity provider's discovery, token, and JWKS endpoints to exercise the full controller and model logic (state management, token exchange, ID token verification, user matching, case-insensitive lookup, email verification) without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, username input, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC_Flow_Updated.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present when accessed. In production, invalid configuration raises &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to prevent startup with a misconfigured provider. In other environments, invalid providers are skipped with a warning to avoid blocking local development and CI where OIDC may not be fully configured. Validation runs at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt; so issues surface immediately in production.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id and username. Both parameters are required; missing parameters return a 400. Fetch the provider's &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve endpoints and JWKS keys. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, username, and creation timestamp. With a 10% probability per request, enqueue a &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt; to amortize the cost of deleting stale rows without requiring a dedicated scheduler. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that accepts the authorization code and state. Both parameters are required. Atomically look up and destroy the matching &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row by state within a database transaction with row-level locking, rejecting the request if no row is found or if the row is older than 5 minutes. The atomic consume prevents replay. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. The &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim must be explicitly &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;; tokens without it or with a false value are rejected. Match the user by both the stored username and the verified email claim from the ID token using case-insensitive, whitespace-trimmed comparison (emails are not unique in Expertiza, so username disambiguates). If a match is found, issue a session JWT via &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt; — the same method used by the existing password login. Return a generic 401 &amp;quot;Authentication failed&amp;quot; for all verification or matching failures (invalid state, replayed state, token verification failure, unverified email, no matching user, unknown provider) to avoid leaking which specific check failed.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):'''&lt;br /&gt;
** On page load, the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;&lt;br /&gt;
*** If the request fails or returns empty, the component renders nothing, and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
*** If providers are found, an SSO login button is displayed.&lt;br /&gt;
** Once the SSO Button is clicked, a modal displays with a username field and a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider.&lt;br /&gt;
&lt;br /&gt;
[[File:LoginPageWithSSOButton.png|1000px|Login Page with SSO Button]]&lt;br /&gt;
[[File:SSOLoginModal.png|1000px|SSO Login Modal]]&lt;br /&gt;
&lt;br /&gt;
* '''Initiate Login (Step 2):''' Once the user enters their username, selects a provider, and clicks &amp;quot;Continue with SSO&amp;quot;, the form posts to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with both the provider id and username. On success, the browser is redirected to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend, and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, we store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
&lt;br /&gt;
* '''Strategy pattern''' — Each identity provider is defined as a YAML configuration block with its own credentials, issuer, and scopes. &amp;lt;code&amp;gt;OidcRequest.authorization_uri_for!&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; operate on whatever provider key is passed in, with no &amp;lt;code&amp;gt;if provider == &amp;quot;google&amp;quot;&amp;lt;/code&amp;gt; branching. Adding a new institution's SSO requires only a new YAML entry and environment variables, no code changes anywhere.&lt;br /&gt;
** [https://github.com/johnmweisz/reimplementation-back-end/blob/9bc24c3ca0a13188899b3b72ec3208afcdd8a21b/app/controllers/oidc_login_controller.rb#L34 Source]&lt;br /&gt;
&lt;br /&gt;
* '''Singleton''' — &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; is effectively a singleton: provider configuration is memoized as class-level state and cleared via &amp;lt;code&amp;gt;reload!&amp;lt;/code&amp;gt;, ensuring YAML is parsed once per process. Ruby's &amp;lt;code&amp;gt;Singleton&amp;lt;/code&amp;gt; module was not used because it would require changing every call site from &amp;lt;code&amp;gt;OidcConfig.find(...)&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;OidcConfig.instance.find(...)&amp;lt;/code&amp;gt; without providing additional safety since necessary methods are not available to a new instance anyway.  AI recommended we keep it this way.&lt;br /&gt;
&lt;br /&gt;
* '''Probabilistic Garbage Collection''' — Stale &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; rows are cleaned up on a small percentage (10%) of new request creations rather than scheduled. This amortizes cleanup cost across normal usage and avoids adding a scheduler dependency to the application.&lt;br /&gt;
&lt;br /&gt;
=== Schema (OidcRequest) ===&lt;br /&gt;
The &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || not null, unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; || string || not null || Expertiza username entered before login; used alongside the verified email claim to match an existing user (emails are not unique)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
No foreign keys or associations to other tables. Stale rows are cleaned up probabilistically on new request creation (10% chance to enqueue &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt;), avoiding the need for a dedicated scheduler.&lt;br /&gt;
&lt;br /&gt;
=== Provider Configuration (OidcConfig) ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; model loads OIDC identity provider definitions from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; at boot. Each provider is defined as a keyed entry under &amp;lt;code&amp;gt;providers:&amp;lt;/code&amp;gt;. The top-level key is the provider id used in API requests and stored in the &amp;lt;code&amp;gt;oidc_requests.provider&amp;lt;/code&amp;gt; column. Client credentials are injected from environment variables via ERB to keep secrets out of version control.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Key !! Required !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| ''provider key'' (e.g. &amp;lt;code&amp;gt;google-ncsu&amp;lt;/code&amp;gt;) || yes || Unique identifier for this provider. Sent by the frontend in &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; and stored on the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row. Use a short, URL-safe slug.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt; || yes || Human-readable name shown to users in the login dropdown (e.g. &amp;quot;Google NCSU&amp;quot;).&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt; || yes || The OIDC issuer URL (e.g. &amp;lt;code&amp;gt;https://accounts.google.com&amp;lt;/code&amp;gt;). Used to fetch the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; discovery document, which provides the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Must match the &amp;lt;code&amp;gt;iss&amp;lt;/code&amp;gt; claim in ID tokens issued by this provider.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt; || yes || OAuth client identifier obtained when registering the application with the identity provider. Sent in the authorization request and token exchange. Typically injected via &amp;lt;code&amp;gt;&amp;lt;%= ENV['PROVIDER_CLIENT_ID'] %&amp;gt;&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt; || yes || OAuth client secret obtained during registration. Used to authenticate the backend to the token endpoint. Must be kept secret — always injected via &amp;lt;code&amp;gt;&amp;lt;%= ENV['PROVIDER_CLIENT_SECRET'] %&amp;gt;&amp;lt;/code&amp;gt;, never hardcoded.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt; || yes || The URL the identity provider redirects to after authentication. Must exactly match the value registered with the provider (scheme, host, port, and path). Should point to the frontend callback route (e.g. &amp;lt;code&amp;gt;http://localhost:3000/auth/callback&amp;lt;/code&amp;gt;).&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt; || no || Space-separated OIDC scopes requested from the provider. Defaults to &amp;lt;code&amp;gt;openid email profile&amp;lt;/code&amp;gt; if omitted. The &amp;lt;code&amp;gt;openid&amp;lt;/code&amp;gt; scope is required to receive an ID token; &amp;lt;code&amp;gt;email&amp;lt;/code&amp;gt; is required for account matching.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; exposes &amp;lt;code&amp;gt;find(provider_key)&amp;lt;/code&amp;gt; for internal lookups and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; for the frontend-facing &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; response (which only includes id and display name, never secrets or endpoints). Providers missing any required key are handled based on environment: in production, startup fails with &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to prevent running with misconfigured providers; in other environments, the invalid provider is skipped with a warning so local development and CI are not blocked. Discovery is always used; non-discovery providers are not supported. The configuration is validated once at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
providers:&lt;br /&gt;
  google-ncsu:&lt;br /&gt;
    display_name: Google NCSU&lt;br /&gt;
    issuer: https://accounts.google.com&lt;br /&gt;
    client_id: &amp;lt;%= ENV['GOOG_CLIENT_ID'] %&amp;gt;&lt;br /&gt;
    client_secret: &amp;lt;%= ENV['GOOG_CLIENT_SECRET'] %&amp;gt;&lt;br /&gt;
    redirect_uri: &amp;lt;%= ENV['GOOG_REDIRECT_URI'] %&amp;gt;&lt;br /&gt;
    scopes: openid email profile&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== New Dependencies ==&lt;br /&gt;
&lt;br /&gt;
=== openid_connect ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
=== rack-attack ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; gem ([https://github.com/rack/rack-attack github.com/rack/rack-attack]) was added to provide rate limiting on the OIDC endpoints. Without it, an attacker could spam &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; to fill the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table or repeatedly probe &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; to attempt token guessing.&lt;br /&gt;
&lt;br /&gt;
* '''Throttle abuse before it reaches the application:''' Rack-level middleware rejects abusive clients before any controller code runs, protecting both the database and the IdP discovery endpoints from being hammered.&lt;br /&gt;
* '''Per-IP throttling on OIDC endpoints:''' &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; is rate-limited to prevent table-fill attacks and discovery spam (each request creates a row and triggers an IdP discovery call). &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; is rate-limited to slow brute-force state guessing — even though the state values have 256 bits of entropy and won't realistically be guessed, throttling prevents wasted work and log noise.&lt;br /&gt;
* '''Lightweight and well-established:''' Single gem, no external dependencies (uses Rails cache for storage), maintained by the Rack team, used by tens of thousands of production Rails apps.&lt;br /&gt;
* '''Configuration in code:''' Rules live in &amp;lt;code&amp;gt;config/initializers/rack_attack.rb&amp;lt;/code&amp;gt; and are version-controlled alongside the rest of the auth configuration, rather than living in a gateway or load balancer config.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is that rate limiting at the application layer is best-effort — a sufficiently distributed attack can still overwhelm the app. For production deployment, a gateway-level rate limit (e.g. nginx, Cloudflare) should be added in front of &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; as defense in depth. &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; is documented as the application-layer baseline, not the only line of defense.&lt;br /&gt;
&lt;br /&gt;
== File Diffs ==&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: [https://github.com/expertiza/reimplementation-back-end/pull/335 reimplementation-back-end#335]&lt;br /&gt;
* Frontend: [https://github.com/expertiza/reimplementation-front-end/pull/172 reimplementation-front-end#172]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
 app/controllers/oidc_login_controller.rb         — Thin controller for providers, client_select, and callback actions with centralized error handling&lt;br /&gt;
 app/models/oidc_request.rb                       — ActiveRecord model owning state/nonce/PKCE/username storage, OIDC flow, account matching, and probabilistic stale cleanup&lt;br /&gt;
 app/models/oidc_config.rb                        — YAML config loader with validation (strict in production), scope normalization, and public_list filtering&lt;br /&gt;
 app/models/user.rb                               — Added &amp;lt;code&amp;gt;generate_jwt&amp;lt;/code&amp;gt; method shared with password login&lt;br /&gt;
 app/jobs/cleanup_stale_oidc_requests_job.rb      — ActiveJob that calls &amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;&lt;br /&gt;
 config/oidc_providers.yml                        — Provider configuration (ERB for env var injection)&lt;br /&gt;
 config/initializers/oidc.rb                      — Boot-time config validation&lt;br /&gt;
 config/routes.rb                                 — New routes for the three OIDC endpoints&lt;br /&gt;
 db/migrate/*_create_oidc_requests.rb             — Migration for oidc_requests table&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
 src/components/Modals/OidcModal.tsx         — Modal displaying the SSO button and provider dropdown with username input&lt;br /&gt;
 src/pages/OidcCallback/OidcCallback.tsx     — Callback page handling code exchange and auth state dispatch&lt;br /&gt;
 src/pages/Authentication/Login.tsx          — Existing login page with the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component added below the password form&lt;br /&gt;
 src/App.tsx                                 — Added &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 /auth/callback            → React OidcCallback component (frontend route)&lt;br /&gt;
&lt;br /&gt;
== Tests (74 total) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) ===&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_request_spec.rb spec/models/oidc_request_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''.consume_recent_by_state!'''&lt;br /&gt;
* Returns and destroys a recent request matching state&lt;br /&gt;
* Raises RecordNotFound for unknown state&lt;br /&gt;
* Raises RecordNotFound for expired requests and preserves the row&lt;br /&gt;
* Prevents replay by destroying the row on consumption&lt;br /&gt;
&lt;br /&gt;
'''.delete_stale'''&lt;br /&gt;
* Deletes rows older than the validity window and preserves fresh rows&lt;br /&gt;
&lt;br /&gt;
'''Probabilistic cleanup on create'''&lt;br /&gt;
* Enqueues CleanupStaleOidcRequestsJob when rand falls under the threshold&lt;br /&gt;
* Does not enqueue when rand falls above the threshold&lt;br /&gt;
&lt;br /&gt;
'''.authorization_uri_for!'''&lt;br /&gt;
* Creates an oidc_requests row with username and returns provider authorization URI&lt;br /&gt;
* Uses default scopes when provider scopes are missing&lt;br /&gt;
* .authorization_uri_for! Raises a uniqueness error when two rows have the same state&lt;br /&gt;
&lt;br /&gt;
'''#verified_email_from_code!'''&lt;br /&gt;
* Exchanges code, verifies token, and returns email when email_verified is true&lt;br /&gt;
* Raises AuthenticationError when email_verified claim is absent&lt;br /&gt;
* Raises AuthenticationError when email_verified is false&lt;br /&gt;
* #verified_email_from_code! Raises InvalidToken when the token's nonce doesn't match (tampered/replayed token)&lt;br /&gt;
&lt;br /&gt;
'''#authenticate_user!'''&lt;br /&gt;
* Matches user by exact username and email&lt;br /&gt;
* Matches case-insensitively on username&lt;br /&gt;
* Matches case-insensitively on email&lt;br /&gt;
* Matches case-insensitively on both fields&lt;br /&gt;
* Matches user when DB stores values with leading or trailing whitespace&lt;br /&gt;
* Raises AuthenticationError when email is blank&lt;br /&gt;
* Raises AuthenticationError when email matches but username does not&lt;br /&gt;
* Raises AuthenticationError when username matches but email does not&lt;br /&gt;
* Raises AuthenticationError when neither username nor email match&lt;br /&gt;
&lt;br /&gt;
'''.new_client'''&lt;br /&gt;
* Builds an OpenIDConnect::Client with provider credentials and discovery endpoints&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''.providers'''&lt;br /&gt;
* Loads providers from YAML and evaluates ERB env vars&lt;br /&gt;
* Memoizes results until reload! is called&lt;br /&gt;
* Skips providers missing required keys and warns&lt;br /&gt;
* Returns an empty hash when no providers key exists&lt;br /&gt;
* Returns an empty hash when YAML is empty&lt;br /&gt;
* Returns an empty hash when providers key is null&lt;br /&gt;
* Returns an empty hash when the top-level YAML is not a Hash&lt;br /&gt;
* Returns an empty hash when the providers value is not a Hash&lt;br /&gt;
* Supports YAML aliases in provider definitions&lt;br /&gt;
&lt;br /&gt;
'''Production behavior'''&lt;br /&gt;
* Raises InvalidConfiguration when a provider is missing required keys&lt;br /&gt;
* Raises InvalidConfiguration when the top-level YAML is not a Hash&lt;br /&gt;
* Raises InvalidConfiguration when the providers value is not a Hash&lt;br /&gt;
&lt;br /&gt;
'''.find'''&lt;br /&gt;
* Returns a provider config by key&lt;br /&gt;
* Raises ProviderNotFound for unknown provider keys&lt;br /&gt;
&lt;br /&gt;
'''.public_list'''&lt;br /&gt;
* Returns only id and name for each provider, never secrets&lt;br /&gt;
&lt;br /&gt;
'''.scopes_for'''&lt;br /&gt;
* Parses whitespace-delimited scope strings&lt;br /&gt;
* Parses comma-delimited scope strings&lt;br /&gt;
* Parses mixed comma and whitespace delimiters&lt;br /&gt;
* Falls back to default scopes when scopes is nil&lt;br /&gt;
* Falls back to default scopes when scopes key is absent&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/user_spec.rb spec/models/user_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''#generate_jwt'''&lt;br /&gt;
* Encodes the user attributes (id, name, full_name, role, institution_id, exp) into a JWT&lt;br /&gt;
* Defaults to 24 hour expiry&lt;br /&gt;
* Raises an error when the token signature is invalid (tampered token rejected)&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* Returns authorization URL for a valid provider and username&lt;br /&gt;
* Returns 400 when required parameters are missing&lt;br /&gt;
* Returns 404 for unknown provider&lt;br /&gt;
* Returns 502 when provider discovery fails&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — happy path'''&lt;br /&gt;
* Exchanges valid code and state for a session JWT&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — generic 401 &amp;quot;Authentication failed&amp;quot;'''&lt;br /&gt;
* When no user matches the username and email&lt;br /&gt;
* When email matches but username does not&lt;br /&gt;
* When state is invalid or expired&lt;br /&gt;
* When token verification fails&lt;br /&gt;
* When the stored provider no longer exists in config&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — other errors'''&lt;br /&gt;
* Returns 400 when required parameters are missing&lt;br /&gt;
* Returns 502 when provider discovery fails&lt;br /&gt;
&lt;br /&gt;
'''Rate limiting (Rack::Attack)'''&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: 5 requests from one IP all succeed (within limit)&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: 6th request from same IP gets 429 with &amp;quot;Rate limit exceeded&amp;quot;&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: throttled response includes a Retry-After header&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: hitting the limit on one IP does not affect a different IP&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: 10 requests from one IP all succeed (within limit)&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: 11th request from same IP gets 429 with &amp;quot;Rate limit exceeded&amp;quot;&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: throttled response includes a Retry-After header&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) ===&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/Modals/OidcModal.test.tsx src/components/Modals/OidcModal.test.tsx] ====&lt;br /&gt;
&lt;br /&gt;
'''OidcModal Component'''&lt;br /&gt;
* Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* Renders nothing when GET /auth/providers fails&lt;br /&gt;
* Renders SSO button when providers are returned&lt;br /&gt;
* Opens the modal when the SSO button is pressed&lt;br /&gt;
* Populates the provider dropdown with configured providers&lt;br /&gt;
* Disables submit until both username and provider are provided&lt;br /&gt;
* Posts provider id and username to /auth/client-select on submit&lt;br /&gt;
* Redirects the browser to the returned authorization URL on success&lt;br /&gt;
* Does not redirect when client-select fails&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx] ====&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* Stores session JWT and dispatches auth state on success&lt;br /&gt;
* Redirects to dashboard on successful login&lt;br /&gt;
* Displays error alert and redirects to login on backend failure&lt;br /&gt;
* Handles IdP error query parameter without calling the backend&lt;br /&gt;
* Redirects to login when code or state query parameters are missing&lt;br /&gt;
* Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
== Planning ==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/users/johnmweisz/projects/9 Backend project board]&lt;br /&gt;
* [https://github.com/users/johnmweisz/projects/8 Frontend project board]&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML, exposing methods to list providers, look up a provider by key, and normalize scopes.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;.&lt;br /&gt;
* In production, raise &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to block startup with misconfigured providers; in other environments, skip invalid providers with a warning so local development and CI are not blocked.&lt;br /&gt;
* Validate configuration at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt; so issues surface immediately on deploy.&lt;br /&gt;
* Add unit tests for config loading, validation, missing key detection, scope normalization, and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; secrets exclusion.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — OIDC Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, PKCE code verifier, and username, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, not null, unique, indexed), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; (not null), and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt; model with a &amp;lt;code&amp;gt;consume_recent_by_state!&amp;lt;/code&amp;gt; method that atomically finds, locks, and destroys the row in a transaction to prevent replay.&lt;br /&gt;
* Expose a &amp;lt;code&amp;gt;delete_stale&amp;lt;/code&amp;gt; class method that deletes rows older than the validity window.&lt;br /&gt;
* Add unit tests for creation, atomic consumption, expiry, replay prevention, and stale deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider and username, and returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept required &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; params (return 400 on missing params).&lt;br /&gt;
* Look up the provider config and fetch the discovery document.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier and challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; with state, nonce, code_verifier, provider, and username.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Return a 502 if provider discovery fails.&lt;br /&gt;
* Add request specs covering the happy path, missing params, unknown provider, and discovery failure.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept required &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params (return 400 on missing params).&lt;br /&gt;
* Atomically consume the matching &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row by state, rejecting if not found, expired, or already consumed.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, audience (client_id), and nonce.&lt;br /&gt;
* Reject the login if the &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim is not explicitly &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Match an existing user by username (from &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt;) and email (from ID token), case-insensitive with whitespace trimmed on both.&lt;br /&gt;
* On match: issue a session JWT via &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt; and return &amp;lt;code&amp;gt;{ token }&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Return a generic 401 &amp;quot;Authentication failed&amp;quot; for all verification and matching failures to avoid information leakage.&lt;br /&gt;
* Return a 502 if provider discovery fails.&lt;br /&gt;
* Add request specs covering the happy path, missing params, invalid/expired state, replay, token verification failure, username/email mismatch, unverified email, and unknown provider.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — SSO Modal with Username and Provider Selection ===&lt;br /&gt;
'''As a''' user, '''I want''' to enter my username and select a provider on the login page, '''so that''' I can authenticate with my school credentials against the correct Expertiza account.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Display an SSO button when providers are returned.&lt;br /&gt;
* On button press, open a modal with a username text input and a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown populated with the configured providers.&lt;br /&gt;
* Hide or disable the submit action until both username and provider are provided.&lt;br /&gt;
* If the providers request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers, form validation, and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' submitting the SSO form to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On submit, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id and username.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the payload, redirect, and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Unified Session Response ===&lt;br /&gt;
'''As a''' developer, '''I want''' session token generation shared by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (&amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Update &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; to use the shared method without changing its external response shape.&lt;br /&gt;
* Use the shared method in &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;User#generate_jwt&amp;lt;/code&amp;gt; covering payload structure, default and custom expiry, and rejection of tampered tokens.&lt;br /&gt;
* Verify existing password login request specs still pass.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Externalize Hardcoded Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' the frontend API base URL moved to configuration, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the API base URL (currently &amp;lt;code&amp;gt;http://localhost:3002&amp;lt;/code&amp;gt;) to an environment variable (e.g. &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Replace all hardcoded references in &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;Login&amp;lt;/code&amp;gt; components.&lt;br /&gt;
* Document the variable in the README.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
=== Story 11: Backend — Swagger Documentation for OIDC Endpoints ===&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
=== Story 12: Backend — Probabilistic Cleanup of Stale OIDC Requests ===&lt;br /&gt;
'''As a''' developer, '''I want''' stale OIDC request rows cleaned up automatically without a scheduled job, '''so that''' the table does not grow unbounded from abandoned login attempts and no additional infrastructure is required.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt; ActiveJob that calls &amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On &amp;lt;code&amp;gt;after_create&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt;, enqueue the job with a 10% probability (&amp;lt;code&amp;gt;CLEANUP_PROBABILITY&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Use a &amp;lt;code&amp;gt;VALIDITY_WINDOW&amp;lt;/code&amp;gt; constant so the cleanup threshold matches the consumption window.&lt;br /&gt;
* Add tests verifying stale rows are deleted, fresh rows are preserved, and the job is enqueued at the expected probability.&lt;br /&gt;
&lt;br /&gt;
=== Story 13: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' RSpec coverage for the OIDC backend, '''so that''' I have confidence the endpoints, models, and security checks work correctly. Many of these tests are added incrementally alongside each feature story; this story captures the consolidated coverage expectation and gap analysis.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Stub the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Request specs for the three OIDC endpoints covering happy paths and all documented error responses (400, 401, 404, 502).&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt; covering atomic state consumption, replay prevention, expiry, probabilistic cleanup enqueuing, case-insensitive user matching with whitespace normalization, strict &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; handling, and PKCE code verifier flow.&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering YAML loading, ERB interpolation, memoization and reload, missing key detection (warn in dev, raise in production), scope normalization, and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; secrets exclusion.&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;User#generate_jwt&amp;lt;/code&amp;gt; covering payload structure, default and custom expiry, and rejection of tampered tokens.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; specs still pass unchanged.&lt;br /&gt;
&lt;br /&gt;
=== Story 14: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' Vitest coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly. Many of these tests are added incrementally alongside each feature story; this story captures the consolidated coverage expectation and gap analysis.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Component tests for &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Renders nothing on empty or failed providers response.&lt;br /&gt;
** Renders SSO button when providers are returned.&lt;br /&gt;
** Opens the modal when the SSO button is pressed.&lt;br /&gt;
** Populates the provider dropdown with configured providers.&lt;br /&gt;
** Requires both username and provider before submit is enabled.&lt;br /&gt;
** Includes provider id and username in the &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; payload.&lt;br /&gt;
** Redirects the browser to the returned authorization URL on success.&lt;br /&gt;
** Does not redirect on failure.&lt;br /&gt;
* Component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Posts code and state to &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
** Stores the session JWT and dispatches auth state on success.&lt;br /&gt;
** Redirects to the dashboard on success.&lt;br /&gt;
** Displays an error alert and redirects to login on backend failure.&lt;br /&gt;
** Handles the IdP &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; query parameter without calling the backend.&lt;br /&gt;
** Redirects to login when code or state are missing.&lt;br /&gt;
** Shows a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Story 15: Backend — Rate Limiting on OIDC Endpoints ===&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC endpoints rate-limited at the application layer, '''so that''' abusive clients cannot fill the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table, hammer the IdP discovery endpoint, or brute-force callback state values.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add the &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; gem to the Gemfile.&lt;br /&gt;
* Configure throttles in &amp;lt;code&amp;gt;config/initializers/rack_attack.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
** &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: throttled per IP (e.g. 10 requests per minute) to prevent table-fill and discovery-spam attacks.&lt;br /&gt;
** &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: throttled per IP (e.g. 20 requests per minute) to slow brute-force state guessing.&lt;br /&gt;
* Throttled requests return HTTP 429 with a JSON body matching the existing error response shape (&amp;lt;code&amp;gt;{ error: &amp;quot;Too many requests&amp;quot; }&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Use the Rails cache (&amp;lt;code&amp;gt;Rails.cache&amp;lt;/code&amp;gt;) as the backing store so no additional infrastructure is required.&lt;br /&gt;
* Document the rationale in the initializer with a brief comment, including the note that gateway-level rate limiting (nginx, Cloudflare) should be added in front for defense in depth in production.&lt;br /&gt;
* Add request specs verifying:&lt;br /&gt;
** A burst of requests above the threshold returns 429.&lt;br /&gt;
** Requests under the threshold succeed normally.&lt;br /&gt;
** Different IPs are throttled independently.&lt;br /&gt;
* Verify existing endpoints (login, etc.) are not affected by the new rules.&lt;br /&gt;
&lt;br /&gt;
== NCSU Google Provider Setup ==&lt;br /&gt;
&lt;br /&gt;
You can find more details about how to set up the Google OIDC Provider at [https://wiki.expertiza.ncsu.edu/index.php?title=Google_OIDC_Setup Google OIDC Setup]&lt;br /&gt;
&lt;br /&gt;
== Demo Video ==&lt;br /&gt;
&lt;br /&gt;
You can view the feature in action as well as edge cases, tests, and swagger by watching the [https://youtu.be/ES6VWZxkj_w| Demo Video]&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=168087</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=168087"/>
		<updated>2026-04-26T01:48:22Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
&lt;br /&gt;
Expertiza currently authenticates users with its own login page, implemented by the Expertiza application. Expertiza has been used at many campuses, however, and each has their own SSO (single signon) protocol that students and staff use to log into other applications. Supporting these standard protocols at sites where they are in use is more secure for the application, provides a familiar and streamlined login experience, and frees Expertiza from managing credentials for users whose institution already does so. This design introduces [https://openid.net/developers/how-connect-works/ OIDC] login as an additional authentication option alongside the existing username and password login. Both methods will continue to be supported, allowing users to choose their preferred approach.&lt;br /&gt;
&lt;br /&gt;
== Design Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users enter their Expertiza username on the login page and select a provider from a dropdown. The frontend posts the username and provider to the backend, which returns an authorization URL. The user is redirected to the school's OIDC provider, authenticates, and is redirected back to the frontend callback. The callback posts the authorization code and state to the backend to complete login. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Matching ===&lt;br /&gt;
Match the authenticated user by both the Expertiza username (provided before login) and the verified email claim from the ID token. Username is required because email addresses are not unique in Expertiza; multiple accounts may share the same email. Matching is case-insensitive on both fields. The provider must validate email with the &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim and if it is not &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the login is rejected. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, a generic authentication error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
* OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Client credentials (client ID, client secret) are stored in environment variables and injected via ERB.&lt;br /&gt;
* Providers must support OIDC discovery;&lt;br /&gt;
** Their endpoints and JWKS keys are fetched automatically from the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document.&lt;br /&gt;
* The system supports multiple OIDC provider configurations simultaneously.&lt;br /&gt;
* Provider configuration is validated at boot via config/initializers/oidc.rb. In production, a provider with any missing required key raises OidcConfig::InvalidConfiguration, which prevents the application from starting with a misconfigured OIDC provider. In all other environments (development, test), the invalid provider is skipped with a warning logged so that local development and CI are not blocked when OIDC credentials are not configured.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, PKCE code verifier, username, and provider key are stored server-side in an &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and consumed (deleted) on successful callback. A probabilistic inline cleanup removes stale rows on new request creation to keep the table bounded without requiring a scheduled job. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Login ===&lt;br /&gt;
At one point it was suggested that OIDC completely replace password login if configured.  We considered this idea, however, given this feature must support multiple institutions, some of which may want OIDC and others which may not and there is no institution context pre-login at this time, we felt that probably falls under account management and is beyond the scope of this assignment.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted. The OIDC flow is only used to verify the user's identity with an external provider at login time. Once the user is authenticated, Expertiza issues its own session JWT, and all subsequent requests use that local session. The IdP session is independent of the Expertiza session, so logging out of Expertiza (destroying the local session) does not affect the user's session at the IdP, and logging out of the IdP does not affect the user's Expertiza session. RP-initiated logout (ending the IdP session as part of application logout) is out of scope.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
All callback failure modes (invalid state, expired state, replayed state, no matching user, mismatched username or email, failed token verification, unverified email, unknown provider) return a generic &amp;quot;Authentication failed&amp;quot; response with HTTP 401 to avoid leaking information about which specific check failed. Provider communication failures (discovery or token endpoint unreachable) return HTTP 502. Missing required parameters return HTTP 400. Unknown providers on &amp;lt;code&amp;gt;client-select&amp;lt;/code&amp;gt; return HTTP 404.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. State rows are atomically consumed in a database transaction with row-level locking to prevent race conditions on replay. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE. The &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim is required and must be true.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request and model specs stub the identity provider's discovery, token, and JWKS endpoints to exercise the full controller and model logic (state management, token exchange, ID token verification, user matching, case-insensitive lookup, email verification) without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, username input, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC_Flow_Updated.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present when accessed. In production, invalid configuration raises &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to prevent startup with a misconfigured provider. In other environments, invalid providers are skipped with a warning to avoid blocking local development and CI where OIDC may not be fully configured. Validation runs at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt; so issues surface immediately in production.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id and username. Both parameters are required; missing parameters return a 400. Fetch the provider's &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve endpoints and JWKS keys. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, username, and creation timestamp. With a 10% probability per request, enqueue a &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt; to amortize the cost of deleting stale rows without requiring a dedicated scheduler. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that accepts the authorization code and state. Both parameters are required. Atomically look up and destroy the matching &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row by state within a database transaction with row-level locking, rejecting the request if no row is found or if the row is older than 5 minutes. The atomic consume prevents replay. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. The &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim must be explicitly &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;; tokens without it or with a false value are rejected. Match the user by both the stored username and the verified email claim from the ID token using case-insensitive, whitespace-trimmed comparison (emails are not unique in Expertiza, so username disambiguates). If a match is found, issue a session JWT via &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt; — the same method used by the existing password login. Return a generic 401 &amp;quot;Authentication failed&amp;quot; for all verification or matching failures (invalid state, replayed state, token verification failure, unverified email, no matching user, unknown provider) to avoid leaking which specific check failed.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):'''&lt;br /&gt;
** On page load, the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;&lt;br /&gt;
*** If the request fails or returns empty, the component renders nothing, and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
*** If providers are found, an SSO login button is displayed.&lt;br /&gt;
** Once the SSO Button is clicked, a modal displays with a username field and a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider.&lt;br /&gt;
&lt;br /&gt;
[[File:LoginPageWithSSOButton.png|1000px|Login Page with SSO Button]]&lt;br /&gt;
[[File:SSOLoginModal.png|1000px|SSO Login Modal]]&lt;br /&gt;
&lt;br /&gt;
* '''Initiate Login (Step 2):''' Once the user enters their username, selects a provider, and clicks &amp;quot;Continue with SSO&amp;quot;, the form posts to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with both the provider id and username. On success, the browser is redirected to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend, and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, we store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
&lt;br /&gt;
* '''Strategy pattern''' — Each identity provider is defined as a YAML configuration block with its own credentials, issuer, and scopes. &amp;lt;code&amp;gt;OidcRequest.authorization_uri_for!&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; operate on whatever provider key is passed in, with no &amp;lt;code&amp;gt;if provider == &amp;quot;google&amp;quot;&amp;lt;/code&amp;gt; branching. Adding a new institution's SSO requires only a new YAML entry and environment variables — no code changes anywhere.&lt;br /&gt;
&lt;br /&gt;
* '''Facade''' — OidcLoginController: The controller exposes three simple, clean endpoints (providers, client_select, callback) while hiding the underlying complexity: YAML parsing, PKCE generation, database row management, provider discovery, token exchange, JWKS verification, and user matching all happen inside &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* '''Chain of Responsibility''' — callback error handling: The callback action processes a sequence of steps -&amp;gt; consume state, exchange code, verify token, match user. Each can fail in a different way.&lt;br /&gt;
&lt;br /&gt;
* '''Command''' — CleanupStaleOidcRequestsJob: The work of deleting stale &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; rows is encapsulated as a background job that can be enqueued, retried, or deferred by Active Job. The job itself is a single line (&amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;). This separates the decision of when to clean up (probabilistic, at row creation time) from the act of cleaning up (the job), and means cleanup can be safely moved to a scheduler later with no logic changes.&lt;br /&gt;
&lt;br /&gt;
* '''Null Object (graceful degradation)''' — OidcModal: When &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; fails or returns an empty list, &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; renders nothing at all. The login page behaves exactly as if OIDC were never configured, and the standard username/password form is completely unaffected. This is cleaner than showing a loading spinner or error state, because from the user's perspective there is simply no SSO option instead of a broken one.&lt;br /&gt;
&lt;br /&gt;
* Provider configuration is memoized as class-level state in &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; and cleared via &amp;lt;code&amp;gt;reload!&amp;lt;/code&amp;gt;, ensuring YAML is parsed once per process.&lt;br /&gt;
&lt;br /&gt;
=== Schema (OidcRequest) ===&lt;br /&gt;
The &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || not null, unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; || string || not null || Expertiza username entered before login; used alongside the verified email claim to match an existing user (emails are not unique)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
No foreign keys or associations to other tables. Stale rows are cleaned up probabilistically on new request creation (10% chance to enqueue &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt;), avoiding the need for a dedicated scheduler.&lt;br /&gt;
&lt;br /&gt;
=== Provider Configuration (OidcConfig) ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; model loads OIDC identity provider definitions from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; at boot. Each provider is defined as a keyed entry under &amp;lt;code&amp;gt;providers:&amp;lt;/code&amp;gt;. The top-level key is the provider id used in API requests and stored in the &amp;lt;code&amp;gt;oidc_requests.provider&amp;lt;/code&amp;gt; column. Client credentials are injected from environment variables via ERB to keep secrets out of version control.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Key !! Required !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| ''provider key'' (e.g. &amp;lt;code&amp;gt;google-ncsu&amp;lt;/code&amp;gt;) || yes || Unique identifier for this provider. Sent by the frontend in &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; and stored on the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row. Use a short, URL-safe slug.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt; || yes || Human-readable name shown to users in the login dropdown (e.g. &amp;quot;Google NCSU&amp;quot;).&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt; || yes || The OIDC issuer URL (e.g. &amp;lt;code&amp;gt;https://accounts.google.com&amp;lt;/code&amp;gt;). Used to fetch the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; discovery document, which provides the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Must match the &amp;lt;code&amp;gt;iss&amp;lt;/code&amp;gt; claim in ID tokens issued by this provider.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt; || yes || OAuth client identifier obtained when registering the application with the identity provider. Sent in the authorization request and token exchange. Typically injected via &amp;lt;code&amp;gt;&amp;lt;%= ENV['PROVIDER_CLIENT_ID'] %&amp;gt;&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt; || yes || OAuth client secret obtained during registration. Used to authenticate the backend to the token endpoint. Must be kept secret — always injected via &amp;lt;code&amp;gt;&amp;lt;%= ENV['PROVIDER_CLIENT_SECRET'] %&amp;gt;&amp;lt;/code&amp;gt;, never hardcoded.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt; || yes || The URL the identity provider redirects to after authentication. Must exactly match the value registered with the provider (scheme, host, port, and path). Should point to the frontend callback route (e.g. &amp;lt;code&amp;gt;http://localhost:3000/auth/callback&amp;lt;/code&amp;gt;).&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt; || no || Space-separated OIDC scopes requested from the provider. Defaults to &amp;lt;code&amp;gt;openid email profile&amp;lt;/code&amp;gt; if omitted. The &amp;lt;code&amp;gt;openid&amp;lt;/code&amp;gt; scope is required to receive an ID token; &amp;lt;code&amp;gt;email&amp;lt;/code&amp;gt; is required for account matching.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; exposes &amp;lt;code&amp;gt;find(provider_key)&amp;lt;/code&amp;gt; for internal lookups and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; for the frontend-facing &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; response (which only includes id and display name, never secrets or endpoints). Providers missing any required key are handled based on environment: in production, startup fails with &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to prevent running with misconfigured providers; in other environments, the invalid provider is skipped with a warning so local development and CI are not blocked. Discovery is always used; non-discovery providers are not supported. The configuration is validated once at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
providers:&lt;br /&gt;
  google-ncsu:&lt;br /&gt;
    display_name: Google NCSU&lt;br /&gt;
    issuer: https://accounts.google.com&lt;br /&gt;
    client_id: &amp;lt;%= ENV['GOOG_CLIENT_ID'] %&amp;gt;&lt;br /&gt;
    client_secret: &amp;lt;%= ENV['GOOG_CLIENT_SECRET'] %&amp;gt;&lt;br /&gt;
    redirect_uri: &amp;lt;%= ENV['GOOG_REDIRECT_URI'] %&amp;gt;&lt;br /&gt;
    scopes: openid email profile&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== New Dependencies ==&lt;br /&gt;
&lt;br /&gt;
=== openid_connect ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
=== rack-attack ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; gem ([https://github.com/rack/rack-attack github.com/rack/rack-attack]) was added to provide rate limiting on the OIDC endpoints. Without it, an attacker could spam &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; to fill the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table or repeatedly probe &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; to attempt token guessing.&lt;br /&gt;
&lt;br /&gt;
* '''Throttle abuse before it reaches the application:''' Rack-level middleware rejects abusive clients before any controller code runs, protecting both the database and the IdP discovery endpoints from being hammered.&lt;br /&gt;
* '''Per-IP throttling on OIDC endpoints:''' &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; is rate-limited to prevent table-fill attacks and discovery spam (each request creates a row and triggers an IdP discovery call). &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; is rate-limited to slow brute-force state guessing — even though the state values have 256 bits of entropy and won't realistically be guessed, throttling prevents wasted work and log noise.&lt;br /&gt;
* '''Lightweight and well-established:''' Single gem, no external dependencies (uses Rails cache for storage), maintained by the Rack team, used by tens of thousands of production Rails apps.&lt;br /&gt;
* '''Configuration in code:''' Rules live in &amp;lt;code&amp;gt;config/initializers/rack_attack.rb&amp;lt;/code&amp;gt; and are version-controlled alongside the rest of the auth configuration, rather than living in a gateway or load balancer config.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is that rate limiting at the application layer is best-effort — a sufficiently distributed attack can still overwhelm the app. For production deployment, a gateway-level rate limit (e.g. nginx, Cloudflare) should be added in front of &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; as defense in depth. &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; is documented as the application-layer baseline, not the only line of defense.&lt;br /&gt;
&lt;br /&gt;
== File Diffs ==&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: [https://github.com/expertiza/reimplementation-back-end/pull/335 reimplementation-back-end#335]&lt;br /&gt;
* Frontend: [https://github.com/expertiza/reimplementation-front-end/pull/172 reimplementation-front-end#172]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
 app/controllers/oidc_login_controller.rb         — Thin controller for providers, client_select, and callback actions with centralized error handling&lt;br /&gt;
 app/models/oidc_request.rb                       — ActiveRecord model owning state/nonce/PKCE/username storage, OIDC flow, account matching, and probabilistic stale cleanup&lt;br /&gt;
 app/models/oidc_config.rb                        — YAML config loader with validation (strict in production), scope normalization, and public_list filtering&lt;br /&gt;
 app/models/user.rb                               — Added &amp;lt;code&amp;gt;generate_jwt&amp;lt;/code&amp;gt; method shared with password login&lt;br /&gt;
 app/jobs/cleanup_stale_oidc_requests_job.rb      — ActiveJob that calls &amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;&lt;br /&gt;
 config/oidc_providers.yml                        — Provider configuration (ERB for env var injection)&lt;br /&gt;
 config/initializers/oidc.rb                      — Boot-time config validation&lt;br /&gt;
 config/routes.rb                                 — New routes for the three OIDC endpoints&lt;br /&gt;
 db/migrate/*_create_oidc_requests.rb             — Migration for oidc_requests table&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
 src/components/Modals/OidcModal.tsx         — Modal displaying the SSO button and provider dropdown with username input&lt;br /&gt;
 src/pages/OidcCallback/OidcCallback.tsx     — Callback page handling code exchange and auth state dispatch&lt;br /&gt;
 src/pages/Authentication/Login.tsx          — Existing login page with the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component added below the password form&lt;br /&gt;
 src/App.tsx                                 — Added &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 /auth/callback            → React OidcCallback component (frontend route)&lt;br /&gt;
&lt;br /&gt;
== Tests (74 total) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) ===&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_request_spec.rb spec/models/oidc_request_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''.consume_recent_by_state!'''&lt;br /&gt;
* Returns and destroys a recent request matching state&lt;br /&gt;
* Raises RecordNotFound for unknown state&lt;br /&gt;
* Raises RecordNotFound for expired requests and preserves the row&lt;br /&gt;
* Prevents replay by destroying the row on consumption&lt;br /&gt;
&lt;br /&gt;
'''.delete_stale'''&lt;br /&gt;
* Deletes rows older than the validity window and preserves fresh rows&lt;br /&gt;
&lt;br /&gt;
'''Probabilistic cleanup on create'''&lt;br /&gt;
* Enqueues CleanupStaleOidcRequestsJob when rand falls under the threshold&lt;br /&gt;
* Does not enqueue when rand falls above the threshold&lt;br /&gt;
&lt;br /&gt;
'''.authorization_uri_for!'''&lt;br /&gt;
* Creates an oidc_requests row with username and returns provider authorization URI&lt;br /&gt;
* Uses default scopes when provider scopes are missing&lt;br /&gt;
* .authorization_uri_for! Raises a uniqueness error when two rows have the same state&lt;br /&gt;
&lt;br /&gt;
'''#verified_email_from_code!'''&lt;br /&gt;
* Exchanges code, verifies token, and returns email when email_verified is true&lt;br /&gt;
* Raises AuthenticationError when email_verified claim is absent&lt;br /&gt;
* Raises AuthenticationError when email_verified is false&lt;br /&gt;
* #verified_email_from_code! Raises InvalidToken when the token's nonce doesn't match (tampered/replayed token)&lt;br /&gt;
&lt;br /&gt;
'''#authenticate_user!'''&lt;br /&gt;
* Matches user by exact username and email&lt;br /&gt;
* Matches case-insensitively on username&lt;br /&gt;
* Matches case-insensitively on email&lt;br /&gt;
* Matches case-insensitively on both fields&lt;br /&gt;
* Matches user when DB stores values with leading or trailing whitespace&lt;br /&gt;
* Raises AuthenticationError when email is blank&lt;br /&gt;
* Raises AuthenticationError when email matches but username does not&lt;br /&gt;
* Raises AuthenticationError when username matches but email does not&lt;br /&gt;
* Raises AuthenticationError when neither username nor email match&lt;br /&gt;
&lt;br /&gt;
'''.new_client'''&lt;br /&gt;
* Builds an OpenIDConnect::Client with provider credentials and discovery endpoints&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''.providers'''&lt;br /&gt;
* Loads providers from YAML and evaluates ERB env vars&lt;br /&gt;
* Memoizes results until reload! is called&lt;br /&gt;
* Skips providers missing required keys and warns&lt;br /&gt;
* Returns an empty hash when no providers key exists&lt;br /&gt;
* Returns an empty hash when YAML is empty&lt;br /&gt;
* Returns an empty hash when providers key is null&lt;br /&gt;
* Returns an empty hash when the top-level YAML is not a Hash&lt;br /&gt;
* Returns an empty hash when the providers value is not a Hash&lt;br /&gt;
* Supports YAML aliases in provider definitions&lt;br /&gt;
&lt;br /&gt;
'''Production behavior'''&lt;br /&gt;
* Raises InvalidConfiguration when a provider is missing required keys&lt;br /&gt;
* Raises InvalidConfiguration when the top-level YAML is not a Hash&lt;br /&gt;
* Raises InvalidConfiguration when the providers value is not a Hash&lt;br /&gt;
&lt;br /&gt;
'''.find'''&lt;br /&gt;
* Returns a provider config by key&lt;br /&gt;
* Raises ProviderNotFound for unknown provider keys&lt;br /&gt;
&lt;br /&gt;
'''.public_list'''&lt;br /&gt;
* Returns only id and name for each provider, never secrets&lt;br /&gt;
&lt;br /&gt;
'''.scopes_for'''&lt;br /&gt;
* Parses whitespace-delimited scope strings&lt;br /&gt;
* Parses comma-delimited scope strings&lt;br /&gt;
* Parses mixed comma and whitespace delimiters&lt;br /&gt;
* Falls back to default scopes when scopes is nil&lt;br /&gt;
* Falls back to default scopes when scopes key is absent&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/user_spec.rb spec/models/user_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''#generate_jwt'''&lt;br /&gt;
* Encodes the user attributes (id, name, full_name, role, institution_id, exp) into a JWT&lt;br /&gt;
* Defaults to 24 hour expiry&lt;br /&gt;
* Raises an error when the token signature is invalid (tampered token rejected)&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* Returns authorization URL for a valid provider and username&lt;br /&gt;
* Returns 400 when required parameters are missing&lt;br /&gt;
* Returns 404 for unknown provider&lt;br /&gt;
* Returns 502 when provider discovery fails&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — happy path'''&lt;br /&gt;
* Exchanges valid code and state for a session JWT&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — generic 401 &amp;quot;Authentication failed&amp;quot;'''&lt;br /&gt;
* When no user matches the username and email&lt;br /&gt;
* When email matches but username does not&lt;br /&gt;
* When state is invalid or expired&lt;br /&gt;
* When token verification fails&lt;br /&gt;
* When the stored provider no longer exists in config&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — other errors'''&lt;br /&gt;
* Returns 400 when required parameters are missing&lt;br /&gt;
* Returns 502 when provider discovery fails&lt;br /&gt;
&lt;br /&gt;
'''Rate limiting (Rack::Attack)'''&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: 5 requests from one IP all succeed (within limit)&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: 6th request from same IP gets 429 with &amp;quot;Rate limit exceeded&amp;quot;&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: throttled response includes a Retry-After header&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: hitting the limit on one IP does not affect a different IP&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: 10 requests from one IP all succeed (within limit)&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: 11th request from same IP gets 429 with &amp;quot;Rate limit exceeded&amp;quot;&lt;br /&gt;
* &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: throttled response includes a Retry-After header&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) ===&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/Modals/OidcModal.test.tsx src/components/Modals/OidcModal.test.tsx] ====&lt;br /&gt;
&lt;br /&gt;
'''OidcModal Component'''&lt;br /&gt;
* Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* Renders nothing when GET /auth/providers fails&lt;br /&gt;
* Renders SSO button when providers are returned&lt;br /&gt;
* Opens the modal when the SSO button is pressed&lt;br /&gt;
* Populates the provider dropdown with configured providers&lt;br /&gt;
* Disables submit until both username and provider are provided&lt;br /&gt;
* Posts provider id and username to /auth/client-select on submit&lt;br /&gt;
* Redirects the browser to the returned authorization URL on success&lt;br /&gt;
* Does not redirect when client-select fails&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx] ====&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* Stores session JWT and dispatches auth state on success&lt;br /&gt;
* Redirects to dashboard on successful login&lt;br /&gt;
* Displays error alert and redirects to login on backend failure&lt;br /&gt;
* Handles IdP error query parameter without calling the backend&lt;br /&gt;
* Redirects to login when code or state query parameters are missing&lt;br /&gt;
* Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
== Planning ==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/users/johnmweisz/projects/9 Backend project board]&lt;br /&gt;
* [https://github.com/users/johnmweisz/projects/8 Frontend project board]&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML, exposing methods to list providers, look up a provider by key, and normalize scopes.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;.&lt;br /&gt;
* In production, raise &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to block startup with misconfigured providers; in other environments, skip invalid providers with a warning so local development and CI are not blocked.&lt;br /&gt;
* Validate configuration at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt; so issues surface immediately on deploy.&lt;br /&gt;
* Add unit tests for config loading, validation, missing key detection, scope normalization, and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; secrets exclusion.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — OIDC Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, PKCE code verifier, and username, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, not null, unique, indexed), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; (not null), and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt; model with a &amp;lt;code&amp;gt;consume_recent_by_state!&amp;lt;/code&amp;gt; method that atomically finds, locks, and destroys the row in a transaction to prevent replay.&lt;br /&gt;
* Expose a &amp;lt;code&amp;gt;delete_stale&amp;lt;/code&amp;gt; class method that deletes rows older than the validity window.&lt;br /&gt;
* Add unit tests for creation, atomic consumption, expiry, replay prevention, and stale deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider and username, and returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept required &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; params (return 400 on missing params).&lt;br /&gt;
* Look up the provider config and fetch the discovery document.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier and challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; with state, nonce, code_verifier, provider, and username.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Return a 502 if provider discovery fails.&lt;br /&gt;
* Add request specs covering the happy path, missing params, unknown provider, and discovery failure.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept required &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params (return 400 on missing params).&lt;br /&gt;
* Atomically consume the matching &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row by state, rejecting if not found, expired, or already consumed.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, audience (client_id), and nonce.&lt;br /&gt;
* Reject the login if the &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim is not explicitly &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Match an existing user by username (from &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt;) and email (from ID token), case-insensitive with whitespace trimmed on both.&lt;br /&gt;
* On match: issue a session JWT via &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt; and return &amp;lt;code&amp;gt;{ token }&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Return a generic 401 &amp;quot;Authentication failed&amp;quot; for all verification and matching failures to avoid information leakage.&lt;br /&gt;
* Return a 502 if provider discovery fails.&lt;br /&gt;
* Add request specs covering the happy path, missing params, invalid/expired state, replay, token verification failure, username/email mismatch, unverified email, and unknown provider.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — SSO Modal with Username and Provider Selection ===&lt;br /&gt;
'''As a''' user, '''I want''' to enter my username and select a provider on the login page, '''so that''' I can authenticate with my school credentials against the correct Expertiza account.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Display an SSO button when providers are returned.&lt;br /&gt;
* On button press, open a modal with a username text input and a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown populated with the configured providers.&lt;br /&gt;
* Hide or disable the submit action until both username and provider are provided.&lt;br /&gt;
* If the providers request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers, form validation, and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' submitting the SSO form to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On submit, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id and username.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the payload, redirect, and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Unified Session Response ===&lt;br /&gt;
'''As a''' developer, '''I want''' session token generation shared by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (&amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Update &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; to use the shared method without changing its external response shape.&lt;br /&gt;
* Use the shared method in &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;User#generate_jwt&amp;lt;/code&amp;gt; covering payload structure, default and custom expiry, and rejection of tampered tokens.&lt;br /&gt;
* Verify existing password login request specs still pass.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Externalize Hardcoded Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' the frontend API base URL moved to configuration, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the API base URL (currently &amp;lt;code&amp;gt;http://localhost:3002&amp;lt;/code&amp;gt;) to an environment variable (e.g. &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Replace all hardcoded references in &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;Login&amp;lt;/code&amp;gt; components.&lt;br /&gt;
* Document the variable in the README.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
=== Story 11: Backend — Swagger Documentation for OIDC Endpoints ===&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
=== Story 12: Backend — Probabilistic Cleanup of Stale OIDC Requests ===&lt;br /&gt;
'''As a''' developer, '''I want''' stale OIDC request rows cleaned up automatically without a scheduled job, '''so that''' the table does not grow unbounded from abandoned login attempts and no additional infrastructure is required.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt; ActiveJob that calls &amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On &amp;lt;code&amp;gt;after_create&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt;, enqueue the job with a 10% probability (&amp;lt;code&amp;gt;CLEANUP_PROBABILITY&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Use a &amp;lt;code&amp;gt;VALIDITY_WINDOW&amp;lt;/code&amp;gt; constant so the cleanup threshold matches the consumption window.&lt;br /&gt;
* Add tests verifying stale rows are deleted, fresh rows are preserved, and the job is enqueued at the expected probability.&lt;br /&gt;
&lt;br /&gt;
=== Story 13: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' RSpec coverage for the OIDC backend, '''so that''' I have confidence the endpoints, models, and security checks work correctly. Many of these tests are added incrementally alongside each feature story; this story captures the consolidated coverage expectation and gap analysis.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Stub the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Request specs for the three OIDC endpoints covering happy paths and all documented error responses (400, 401, 404, 502).&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt; covering atomic state consumption, replay prevention, expiry, probabilistic cleanup enqueuing, case-insensitive user matching with whitespace normalization, strict &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; handling, and PKCE code verifier flow.&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering YAML loading, ERB interpolation, memoization and reload, missing key detection (warn in dev, raise in production), scope normalization, and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; secrets exclusion.&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;User#generate_jwt&amp;lt;/code&amp;gt; covering payload structure, default and custom expiry, and rejection of tampered tokens.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; specs still pass unchanged.&lt;br /&gt;
&lt;br /&gt;
=== Story 14: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' Vitest coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly. Many of these tests are added incrementally alongside each feature story; this story captures the consolidated coverage expectation and gap analysis.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Component tests for &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Renders nothing on empty or failed providers response.&lt;br /&gt;
** Renders SSO button when providers are returned.&lt;br /&gt;
** Opens the modal when the SSO button is pressed.&lt;br /&gt;
** Populates the provider dropdown with configured providers.&lt;br /&gt;
** Requires both username and provider before submit is enabled.&lt;br /&gt;
** Includes provider id and username in the &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; payload.&lt;br /&gt;
** Redirects the browser to the returned authorization URL on success.&lt;br /&gt;
** Does not redirect on failure.&lt;br /&gt;
* Component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Posts code and state to &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
** Stores the session JWT and dispatches auth state on success.&lt;br /&gt;
** Redirects to the dashboard on success.&lt;br /&gt;
** Displays an error alert and redirects to login on backend failure.&lt;br /&gt;
** Handles the IdP &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; query parameter without calling the backend.&lt;br /&gt;
** Redirects to login when code or state are missing.&lt;br /&gt;
** Shows a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Story 15: Backend — Rate Limiting on OIDC Endpoints ===&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC endpoints rate-limited at the application layer, '''so that''' abusive clients cannot fill the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table, hammer the IdP discovery endpoint, or brute-force callback state values.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add the &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; gem to the Gemfile.&lt;br /&gt;
* Configure throttles in &amp;lt;code&amp;gt;config/initializers/rack_attack.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
** &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: throttled per IP (e.g. 10 requests per minute) to prevent table-fill and discovery-spam attacks.&lt;br /&gt;
** &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: throttled per IP (e.g. 20 requests per minute) to slow brute-force state guessing.&lt;br /&gt;
* Throttled requests return HTTP 429 with a JSON body matching the existing error response shape (&amp;lt;code&amp;gt;{ error: &amp;quot;Too many requests&amp;quot; }&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Use the Rails cache (&amp;lt;code&amp;gt;Rails.cache&amp;lt;/code&amp;gt;) as the backing store so no additional infrastructure is required.&lt;br /&gt;
* Document the rationale in the initializer with a brief comment, including the note that gateway-level rate limiting (nginx, Cloudflare) should be added in front for defense in depth in production.&lt;br /&gt;
* Add request specs verifying:&lt;br /&gt;
** A burst of requests above the threshold returns 429.&lt;br /&gt;
** Requests under the threshold succeed normally.&lt;br /&gt;
** Different IPs are throttled independently.&lt;br /&gt;
* Verify existing endpoints (login, etc.) are not affected by the new rules.&lt;br /&gt;
&lt;br /&gt;
== NCSU Google Provider Setup ==&lt;br /&gt;
&lt;br /&gt;
You can find more details about how to set up the Google OIDC Provider at [https://wiki.expertiza.ncsu.edu/index.php?title=Google_OIDC_Setup Google OIDC Setup]&lt;br /&gt;
&lt;br /&gt;
== Demo Video ==&lt;br /&gt;
&lt;br /&gt;
You can view the feature in action as well as edge cases, tests, and swagger by watching the  [https://drive.google.com/file/d/1I8UrZVbHRGVYDu4LCYoYVtCCm946jCZp/view?usp=sharing| Demo Video]&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=168086</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=168086"/>
		<updated>2026-04-26T01:47:20Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Tests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
&lt;br /&gt;
Expertiza currently authenticates users with its own login page, implemented by the Expertiza application. Expertiza has been used at many campuses, however, and each has their own SSO (single signon) protocol that students and staff use to log into other applications. Supporting these standard protocols at sites where they are in use is more secure for the application, provides a familiar and streamlined login experience, and frees Expertiza from managing credentials for users whose institution already does so. This design introduces [https://openid.net/developers/how-connect-works/ OIDC] login as an additional authentication option alongside the existing username and password login. Both methods will continue to be supported, allowing users to choose their preferred approach.&lt;br /&gt;
&lt;br /&gt;
== Design Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users enter their Expertiza username on the login page and select a provider from a dropdown. The frontend posts the username and provider to the backend, which returns an authorization URL. The user is redirected to the school's OIDC provider, authenticates, and is redirected back to the frontend callback. The callback posts the authorization code and state to the backend to complete login. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Matching ===&lt;br /&gt;
Match the authenticated user by both the Expertiza username (provided before login) and the verified email claim from the ID token. Username is required because email addresses are not unique in Expertiza; multiple accounts may share the same email. Matching is case-insensitive on both fields. The provider must validate email with the &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim and if it is not &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the login is rejected. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, a generic authentication error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
* OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Client credentials (client ID, client secret) are stored in environment variables and injected via ERB.&lt;br /&gt;
* Providers must support OIDC discovery;&lt;br /&gt;
** Their endpoints and JWKS keys are fetched automatically from the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document.&lt;br /&gt;
* The system supports multiple OIDC provider configurations simultaneously.&lt;br /&gt;
* Provider configuration is validated at boot via config/initializers/oidc.rb. In production, a provider with any missing required key raises OidcConfig::InvalidConfiguration, which prevents the application from starting with a misconfigured OIDC provider. In all other environments (development, test), the invalid provider is skipped with a warning logged so that local development and CI are not blocked when OIDC credentials are not configured.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, PKCE code verifier, username, and provider key are stored server-side in an &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and consumed (deleted) on successful callback. A probabilistic inline cleanup removes stale rows on new request creation to keep the table bounded without requiring a scheduled job. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Login ===&lt;br /&gt;
At one point it was suggested that OIDC completely replace password login if configured.  We considered this idea, however, given this feature must support multiple institutions, some of which may want OIDC and others which may not and there is no institution context pre-login at this time, we felt that probably falls under account management and is beyond the scope of this assignment.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted. The OIDC flow is only used to verify the user's identity with an external provider at login time. Once the user is authenticated, Expertiza issues its own session JWT, and all subsequent requests use that local session. The IdP session is independent of the Expertiza session, so logging out of Expertiza (destroying the local session) does not affect the user's session at the IdP, and logging out of the IdP does not affect the user's Expertiza session. RP-initiated logout (ending the IdP session as part of application logout) is out of scope.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
All callback failure modes (invalid state, expired state, replayed state, no matching user, mismatched username or email, failed token verification, unverified email, unknown provider) return a generic &amp;quot;Authentication failed&amp;quot; response with HTTP 401 to avoid leaking information about which specific check failed. Provider communication failures (discovery or token endpoint unreachable) return HTTP 502. Missing required parameters return HTTP 400. Unknown providers on &amp;lt;code&amp;gt;client-select&amp;lt;/code&amp;gt; return HTTP 404.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. State rows are atomically consumed in a database transaction with row-level locking to prevent race conditions on replay. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE. The &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim is required and must be true.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request and model specs stub the identity provider's discovery, token, and JWKS endpoints to exercise the full controller and model logic (state management, token exchange, ID token verification, user matching, case-insensitive lookup, email verification) without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, username input, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC_Flow_Updated.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present when accessed. In production, invalid configuration raises &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to prevent startup with a misconfigured provider. In other environments, invalid providers are skipped with a warning to avoid blocking local development and CI where OIDC may not be fully configured. Validation runs at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt; so issues surface immediately in production.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id and username. Both parameters are required; missing parameters return a 400. Fetch the provider's &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve endpoints and JWKS keys. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, username, and creation timestamp. With a 10% probability per request, enqueue a &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt; to amortize the cost of deleting stale rows without requiring a dedicated scheduler. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that accepts the authorization code and state. Both parameters are required. Atomically look up and destroy the matching &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row by state within a database transaction with row-level locking, rejecting the request if no row is found or if the row is older than 5 minutes. The atomic consume prevents replay. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. The &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim must be explicitly &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;; tokens without it or with a false value are rejected. Match the user by both the stored username and the verified email claim from the ID token using case-insensitive, whitespace-trimmed comparison (emails are not unique in Expertiza, so username disambiguates). If a match is found, issue a session JWT via &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt; — the same method used by the existing password login. Return a generic 401 &amp;quot;Authentication failed&amp;quot; for all verification or matching failures (invalid state, replayed state, token verification failure, unverified email, no matching user, unknown provider) to avoid leaking which specific check failed.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):'''&lt;br /&gt;
** On page load, the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;&lt;br /&gt;
*** If the request fails or returns empty, the component renders nothing, and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
*** If providers are found, an SSO login button is displayed.&lt;br /&gt;
** Once the SSO Button is clicked, a modal displays with a username field and a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider.&lt;br /&gt;
&lt;br /&gt;
[[File:LoginPageWithSSOButton.png|1000px|Login Page with SSO Button]]&lt;br /&gt;
[[File:SSOLoginModal.png|1000px|SSO Login Modal]]&lt;br /&gt;
&lt;br /&gt;
* '''Initiate Login (Step 2):''' Once the user enters their username, selects a provider, and clicks &amp;quot;Continue with SSO&amp;quot;, the form posts to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with both the provider id and username. On success, the browser is redirected to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend, and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, we store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
&lt;br /&gt;
* '''Strategy pattern''' — Each identity provider is defined as a YAML configuration block with its own credentials, issuer, and scopes. &amp;lt;code&amp;gt;OidcRequest.authorization_uri_for!&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; operate on whatever provider key is passed in, with no &amp;lt;code&amp;gt;if provider == &amp;quot;google&amp;quot;&amp;lt;/code&amp;gt; branching. Adding a new institution's SSO requires only a new YAML entry and environment variables — no code changes anywhere.&lt;br /&gt;
&lt;br /&gt;
* '''Facade''' — OidcLoginController: The controller exposes three simple, clean endpoints (providers, client_select, callback) while hiding the underlying complexity: YAML parsing, PKCE generation, database row management, provider discovery, token exchange, JWKS verification, and user matching all happen inside &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* '''Chain of Responsibility''' — callback error handling: The callback action processes a sequence of steps -&amp;gt; consume state, exchange code, verify token, match user. Each can fail in a different way.&lt;br /&gt;
&lt;br /&gt;
* '''Command''' — CleanupStaleOidcRequestsJob: The work of deleting stale &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; rows is encapsulated as a background job that can be enqueued, retried, or deferred by Active Job. The job itself is a single line (&amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;). This separates the decision of when to clean up (probabilistic, at row creation time) from the act of cleaning up (the job), and means cleanup can be safely moved to a scheduler later with no logic changes.&lt;br /&gt;
&lt;br /&gt;
* '''Null Object (graceful degradation)''' — OidcModal: When &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; fails or returns an empty list, &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; renders nothing at all. The login page behaves exactly as if OIDC were never configured, and the standard username/password form is completely unaffected. This is cleaner than showing a loading spinner or error state, because from the user's perspective there is simply no SSO option instead of a broken one.&lt;br /&gt;
&lt;br /&gt;
* Provider configuration is memoized as class-level state in &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; and cleared via &amp;lt;code&amp;gt;reload!&amp;lt;/code&amp;gt;, ensuring YAML is parsed once per process.&lt;br /&gt;
&lt;br /&gt;
=== Schema (OidcRequest) ===&lt;br /&gt;
The &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || not null, unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; || string || not null || Expertiza username entered before login; used alongside the verified email claim to match an existing user (emails are not unique)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
No foreign keys or associations to other tables. Stale rows are cleaned up probabilistically on new request creation (10% chance to enqueue &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt;), avoiding the need for a dedicated scheduler.&lt;br /&gt;
&lt;br /&gt;
=== Provider Configuration (OidcConfig) ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; model loads OIDC identity provider definitions from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; at boot. Each provider is defined as a keyed entry under &amp;lt;code&amp;gt;providers:&amp;lt;/code&amp;gt;. The top-level key is the provider id used in API requests and stored in the &amp;lt;code&amp;gt;oidc_requests.provider&amp;lt;/code&amp;gt; column. Client credentials are injected from environment variables via ERB to keep secrets out of version control.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Key !! Required !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| ''provider key'' (e.g. &amp;lt;code&amp;gt;google-ncsu&amp;lt;/code&amp;gt;) || yes || Unique identifier for this provider. Sent by the frontend in &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; and stored on the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row. Use a short, URL-safe slug.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt; || yes || Human-readable name shown to users in the login dropdown (e.g. &amp;quot;Google NCSU&amp;quot;).&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt; || yes || The OIDC issuer URL (e.g. &amp;lt;code&amp;gt;https://accounts.google.com&amp;lt;/code&amp;gt;). Used to fetch the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; discovery document, which provides the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Must match the &amp;lt;code&amp;gt;iss&amp;lt;/code&amp;gt; claim in ID tokens issued by this provider.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt; || yes || OAuth client identifier obtained when registering the application with the identity provider. Sent in the authorization request and token exchange. Typically injected via &amp;lt;code&amp;gt;&amp;lt;%= ENV['PROVIDER_CLIENT_ID'] %&amp;gt;&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt; || yes || OAuth client secret obtained during registration. Used to authenticate the backend to the token endpoint. Must be kept secret — always injected via &amp;lt;code&amp;gt;&amp;lt;%= ENV['PROVIDER_CLIENT_SECRET'] %&amp;gt;&amp;lt;/code&amp;gt;, never hardcoded.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt; || yes || The URL the identity provider redirects to after authentication. Must exactly match the value registered with the provider (scheme, host, port, and path). Should point to the frontend callback route (e.g. &amp;lt;code&amp;gt;http://localhost:3000/auth/callback&amp;lt;/code&amp;gt;).&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt; || no || Space-separated OIDC scopes requested from the provider. Defaults to &amp;lt;code&amp;gt;openid email profile&amp;lt;/code&amp;gt; if omitted. The &amp;lt;code&amp;gt;openid&amp;lt;/code&amp;gt; scope is required to receive an ID token; &amp;lt;code&amp;gt;email&amp;lt;/code&amp;gt; is required for account matching.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; exposes &amp;lt;code&amp;gt;find(provider_key)&amp;lt;/code&amp;gt; for internal lookups and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; for the frontend-facing &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; response (which only includes id and display name, never secrets or endpoints). Providers missing any required key are handled based on environment: in production, startup fails with &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to prevent running with misconfigured providers; in other environments, the invalid provider is skipped with a warning so local development and CI are not blocked. Discovery is always used; non-discovery providers are not supported. The configuration is validated once at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
providers:&lt;br /&gt;
  google-ncsu:&lt;br /&gt;
    display_name: Google NCSU&lt;br /&gt;
    issuer: https://accounts.google.com&lt;br /&gt;
    client_id: &amp;lt;%= ENV['GOOG_CLIENT_ID'] %&amp;gt;&lt;br /&gt;
    client_secret: &amp;lt;%= ENV['GOOG_CLIENT_SECRET'] %&amp;gt;&lt;br /&gt;
    redirect_uri: &amp;lt;%= ENV['GOOG_REDIRECT_URI'] %&amp;gt;&lt;br /&gt;
    scopes: openid email profile&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== New Dependencies ==&lt;br /&gt;
&lt;br /&gt;
=== openid_connect ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
=== rack-attack ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; gem ([https://github.com/rack/rack-attack github.com/rack/rack-attack]) was added to provide rate limiting on the OIDC endpoints. Without it, an attacker could spam &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; to fill the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table or repeatedly probe &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; to attempt token guessing.&lt;br /&gt;
&lt;br /&gt;
* '''Throttle abuse before it reaches the application:''' Rack-level middleware rejects abusive clients before any controller code runs, protecting both the database and the IdP discovery endpoints from being hammered.&lt;br /&gt;
* '''Per-IP throttling on OIDC endpoints:''' &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; is rate-limited to prevent table-fill attacks and discovery spam (each request creates a row and triggers an IdP discovery call). &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; is rate-limited to slow brute-force state guessing — even though the state values have 256 bits of entropy and won't realistically be guessed, throttling prevents wasted work and log noise.&lt;br /&gt;
* '''Lightweight and well-established:''' Single gem, no external dependencies (uses Rails cache for storage), maintained by the Rack team, used by tens of thousands of production Rails apps.&lt;br /&gt;
* '''Configuration in code:''' Rules live in &amp;lt;code&amp;gt;config/initializers/rack_attack.rb&amp;lt;/code&amp;gt; and are version-controlled alongside the rest of the auth configuration, rather than living in a gateway or load balancer config.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is that rate limiting at the application layer is best-effort — a sufficiently distributed attack can still overwhelm the app. For production deployment, a gateway-level rate limit (e.g. nginx, Cloudflare) should be added in front of &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; as defense in depth. &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; is documented as the application-layer baseline, not the only line of defense.&lt;br /&gt;
&lt;br /&gt;
== File Diffs ==&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: [https://github.com/expertiza/reimplementation-back-end/pull/335 reimplementation-back-end#335]&lt;br /&gt;
* Frontend: [https://github.com/expertiza/reimplementation-front-end/pull/172 reimplementation-front-end#172]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
 app/controllers/oidc_login_controller.rb         — Thin controller for providers, client_select, and callback actions with centralized error handling&lt;br /&gt;
 app/models/oidc_request.rb                       — ActiveRecord model owning state/nonce/PKCE/username storage, OIDC flow, account matching, and probabilistic stale cleanup&lt;br /&gt;
 app/models/oidc_config.rb                        — YAML config loader with validation (strict in production), scope normalization, and public_list filtering&lt;br /&gt;
 app/models/user.rb                               — Added &amp;lt;code&amp;gt;generate_jwt&amp;lt;/code&amp;gt; method shared with password login&lt;br /&gt;
 app/jobs/cleanup_stale_oidc_requests_job.rb      — ActiveJob that calls &amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;&lt;br /&gt;
 config/oidc_providers.yml                        — Provider configuration (ERB for env var injection)&lt;br /&gt;
 config/initializers/oidc.rb                      — Boot-time config validation&lt;br /&gt;
 config/routes.rb                                 — New routes for the three OIDC endpoints&lt;br /&gt;
 db/migrate/*_create_oidc_requests.rb             — Migration for oidc_requests table&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
 src/components/Modals/OidcModal.tsx         — Modal displaying the SSO button and provider dropdown with username input&lt;br /&gt;
 src/pages/OidcCallback/OidcCallback.tsx     — Callback page handling code exchange and auth state dispatch&lt;br /&gt;
 src/pages/Authentication/Login.tsx          — Existing login page with the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component added below the password form&lt;br /&gt;
 src/App.tsx                                 — Added &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 /auth/callback            → React OidcCallback component (frontend route)&lt;br /&gt;
&lt;br /&gt;
== Tests (74 total) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) ===&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_request_spec.rb spec/models/oidc_request_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''.consume_recent_by_state!'''&lt;br /&gt;
* Returns and destroys a recent request matching state&lt;br /&gt;
* Raises RecordNotFound for unknown state&lt;br /&gt;
* Raises RecordNotFound for expired requests and preserves the row&lt;br /&gt;
* Prevents replay by destroying the row on consumption&lt;br /&gt;
&lt;br /&gt;
'''.delete_stale'''&lt;br /&gt;
* Deletes rows older than the validity window and preserves fresh rows&lt;br /&gt;
&lt;br /&gt;
'''Probabilistic cleanup on create'''&lt;br /&gt;
* Enqueues CleanupStaleOidcRequestsJob when rand falls under the threshold&lt;br /&gt;
* Does not enqueue when rand falls above the threshold&lt;br /&gt;
&lt;br /&gt;
'''.authorization_uri_for!'''&lt;br /&gt;
* Creates an oidc_requests row with username and returns provider authorization URI&lt;br /&gt;
* Uses default scopes when provider scopes are missing&lt;br /&gt;
* .authorization_uri_for! Raises a uniqueness error when two rows have the same state&lt;br /&gt;
&lt;br /&gt;
'''#verified_email_from_code!'''&lt;br /&gt;
* Exchanges code, verifies token, and returns email when email_verified is true&lt;br /&gt;
* Raises AuthenticationError when email_verified claim is absent&lt;br /&gt;
* Raises AuthenticationError when email_verified is false&lt;br /&gt;
* #verified_email_from_code! Raises InvalidToken when the token's nonce doesn't match (tampered/replayed token)&lt;br /&gt;
&lt;br /&gt;
'''#authenticate_user!'''&lt;br /&gt;
* Matches user by exact username and email&lt;br /&gt;
* Matches case-insensitively on username&lt;br /&gt;
* Matches case-insensitively on email&lt;br /&gt;
* Matches case-insensitively on both fields&lt;br /&gt;
* Matches user when DB stores values with leading or trailing whitespace&lt;br /&gt;
* Raises AuthenticationError when email is blank&lt;br /&gt;
* Raises AuthenticationError when email matches but username does not&lt;br /&gt;
* Raises AuthenticationError when username matches but email does not&lt;br /&gt;
* Raises AuthenticationError when neither username nor email match&lt;br /&gt;
&lt;br /&gt;
'''.new_client'''&lt;br /&gt;
* Builds an OpenIDConnect::Client with provider credentials and discovery endpoints&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''.providers'''&lt;br /&gt;
* Loads providers from YAML and evaluates ERB env vars&lt;br /&gt;
* Memoizes results until reload! is called&lt;br /&gt;
* Skips providers missing required keys and warns&lt;br /&gt;
* Returns an empty hash when no providers key exists&lt;br /&gt;
* Returns an empty hash when YAML is empty&lt;br /&gt;
* Returns an empty hash when providers key is null&lt;br /&gt;
* Returns an empty hash when the top-level YAML is not a Hash&lt;br /&gt;
* Returns an empty hash when the providers value is not a Hash&lt;br /&gt;
* Supports YAML aliases in provider definitions&lt;br /&gt;
&lt;br /&gt;
'''Production behavior'''&lt;br /&gt;
* Raises InvalidConfiguration when a provider is missing required keys&lt;br /&gt;
* Raises InvalidConfiguration when the top-level YAML is not a Hash&lt;br /&gt;
* Raises InvalidConfiguration when the providers value is not a Hash&lt;br /&gt;
&lt;br /&gt;
'''.find'''&lt;br /&gt;
* Returns a provider config by key&lt;br /&gt;
* Raises ProviderNotFound for unknown provider keys&lt;br /&gt;
&lt;br /&gt;
'''.public_list'''&lt;br /&gt;
* Returns only id and name for each provider, never secrets&lt;br /&gt;
&lt;br /&gt;
'''.scopes_for'''&lt;br /&gt;
* Parses whitespace-delimited scope strings&lt;br /&gt;
* Parses comma-delimited scope strings&lt;br /&gt;
* Parses mixed comma and whitespace delimiters&lt;br /&gt;
* Falls back to default scopes when scopes is nil&lt;br /&gt;
* Falls back to default scopes when scopes key is absent&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/user_spec.rb spec/models/user_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''#generate_jwt'''&lt;br /&gt;
* Encodes the user attributes (id, name, full_name, role, institution_id, exp) into a JWT&lt;br /&gt;
* Defaults to 24 hour expiry&lt;br /&gt;
* Raises an error when the token signature is invalid (tampered token rejected)&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* Returns authorization URL for a valid provider and username&lt;br /&gt;
* Returns 400 when required parameters are missing&lt;br /&gt;
* Returns 404 for unknown provider&lt;br /&gt;
* Returns 502 when provider discovery fails&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — happy path'''&lt;br /&gt;
* Exchanges valid code and state for a session JWT&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — generic 401 &amp;quot;Authentication failed&amp;quot;'''&lt;br /&gt;
* When no user matches the username and email&lt;br /&gt;
* When email matches but username does not&lt;br /&gt;
* When state is invalid or expired&lt;br /&gt;
* When token verification fails&lt;br /&gt;
* When the stored provider no longer exists in config&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — other errors'''&lt;br /&gt;
* Returns 400 when required parameters are missing&lt;br /&gt;
* Returns 502 when provider discovery fails&lt;br /&gt;
&lt;br /&gt;
'''Rate limiting (Rack::Attack)'''&lt;br /&gt;
* (code)POST /auth/client-select(/code): 5 requests from one IP all succeed (within limit)&lt;br /&gt;
* (code)POST /auth/client-select(/code): 6th request from same IP gets 429 with &amp;quot;Rate limit exceeded&amp;quot;&lt;br /&gt;
* (code)POST /auth/client-select(/code): throttled response includes a Retry-After header&lt;br /&gt;
* (code)POST /auth/client-select(/code): hitting the limit on one IP does not affect a different IP&lt;br /&gt;
* (code)POST /auth/callback(/code): 10 requests from one IP all succeed (within limit)&lt;br /&gt;
* (code)POST /auth/callback(/code): 11th request from same IP gets 429 with &amp;quot;Rate limit exceeded&amp;quot;&lt;br /&gt;
* (code)POST /auth/callback(/code): throttled response includes a Retry-After header&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) ===&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/Modals/OidcModal.test.tsx src/components/Modals/OidcModal.test.tsx] ====&lt;br /&gt;
&lt;br /&gt;
'''OidcModal Component'''&lt;br /&gt;
* Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* Renders nothing when GET /auth/providers fails&lt;br /&gt;
* Renders SSO button when providers are returned&lt;br /&gt;
* Opens the modal when the SSO button is pressed&lt;br /&gt;
* Populates the provider dropdown with configured providers&lt;br /&gt;
* Disables submit until both username and provider are provided&lt;br /&gt;
* Posts provider id and username to /auth/client-select on submit&lt;br /&gt;
* Redirects the browser to the returned authorization URL on success&lt;br /&gt;
* Does not redirect when client-select fails&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx] ====&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* Stores session JWT and dispatches auth state on success&lt;br /&gt;
* Redirects to dashboard on successful login&lt;br /&gt;
* Displays error alert and redirects to login on backend failure&lt;br /&gt;
* Handles IdP error query parameter without calling the backend&lt;br /&gt;
* Redirects to login when code or state query parameters are missing&lt;br /&gt;
* Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
== Planning ==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/users/johnmweisz/projects/9 Backend project board]&lt;br /&gt;
* [https://github.com/users/johnmweisz/projects/8 Frontend project board]&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML, exposing methods to list providers, look up a provider by key, and normalize scopes.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;.&lt;br /&gt;
* In production, raise &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to block startup with misconfigured providers; in other environments, skip invalid providers with a warning so local development and CI are not blocked.&lt;br /&gt;
* Validate configuration at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt; so issues surface immediately on deploy.&lt;br /&gt;
* Add unit tests for config loading, validation, missing key detection, scope normalization, and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; secrets exclusion.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — OIDC Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, PKCE code verifier, and username, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, not null, unique, indexed), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; (not null), and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt; model with a &amp;lt;code&amp;gt;consume_recent_by_state!&amp;lt;/code&amp;gt; method that atomically finds, locks, and destroys the row in a transaction to prevent replay.&lt;br /&gt;
* Expose a &amp;lt;code&amp;gt;delete_stale&amp;lt;/code&amp;gt; class method that deletes rows older than the validity window.&lt;br /&gt;
* Add unit tests for creation, atomic consumption, expiry, replay prevention, and stale deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider and username, and returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept required &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; params (return 400 on missing params).&lt;br /&gt;
* Look up the provider config and fetch the discovery document.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier and challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; with state, nonce, code_verifier, provider, and username.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Return a 502 if provider discovery fails.&lt;br /&gt;
* Add request specs covering the happy path, missing params, unknown provider, and discovery failure.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept required &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params (return 400 on missing params).&lt;br /&gt;
* Atomically consume the matching &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row by state, rejecting if not found, expired, or already consumed.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, audience (client_id), and nonce.&lt;br /&gt;
* Reject the login if the &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim is not explicitly &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Match an existing user by username (from &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt;) and email (from ID token), case-insensitive with whitespace trimmed on both.&lt;br /&gt;
* On match: issue a session JWT via &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt; and return &amp;lt;code&amp;gt;{ token }&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Return a generic 401 &amp;quot;Authentication failed&amp;quot; for all verification and matching failures to avoid information leakage.&lt;br /&gt;
* Return a 502 if provider discovery fails.&lt;br /&gt;
* Add request specs covering the happy path, missing params, invalid/expired state, replay, token verification failure, username/email mismatch, unverified email, and unknown provider.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — SSO Modal with Username and Provider Selection ===&lt;br /&gt;
'''As a''' user, '''I want''' to enter my username and select a provider on the login page, '''so that''' I can authenticate with my school credentials against the correct Expertiza account.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Display an SSO button when providers are returned.&lt;br /&gt;
* On button press, open a modal with a username text input and a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown populated with the configured providers.&lt;br /&gt;
* Hide or disable the submit action until both username and provider are provided.&lt;br /&gt;
* If the providers request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers, form validation, and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' submitting the SSO form to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On submit, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id and username.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the payload, redirect, and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Unified Session Response ===&lt;br /&gt;
'''As a''' developer, '''I want''' session token generation shared by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (&amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Update &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; to use the shared method without changing its external response shape.&lt;br /&gt;
* Use the shared method in &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;User#generate_jwt&amp;lt;/code&amp;gt; covering payload structure, default and custom expiry, and rejection of tampered tokens.&lt;br /&gt;
* Verify existing password login request specs still pass.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Externalize Hardcoded Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' the frontend API base URL moved to configuration, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the API base URL (currently &amp;lt;code&amp;gt;http://localhost:3002&amp;lt;/code&amp;gt;) to an environment variable (e.g. &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Replace all hardcoded references in &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;Login&amp;lt;/code&amp;gt; components.&lt;br /&gt;
* Document the variable in the README.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
=== Story 11: Backend — Swagger Documentation for OIDC Endpoints ===&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
=== Story 12: Backend — Probabilistic Cleanup of Stale OIDC Requests ===&lt;br /&gt;
'''As a''' developer, '''I want''' stale OIDC request rows cleaned up automatically without a scheduled job, '''so that''' the table does not grow unbounded from abandoned login attempts and no additional infrastructure is required.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt; ActiveJob that calls &amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On &amp;lt;code&amp;gt;after_create&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt;, enqueue the job with a 10% probability (&amp;lt;code&amp;gt;CLEANUP_PROBABILITY&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Use a &amp;lt;code&amp;gt;VALIDITY_WINDOW&amp;lt;/code&amp;gt; constant so the cleanup threshold matches the consumption window.&lt;br /&gt;
* Add tests verifying stale rows are deleted, fresh rows are preserved, and the job is enqueued at the expected probability.&lt;br /&gt;
&lt;br /&gt;
=== Story 13: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' RSpec coverage for the OIDC backend, '''so that''' I have confidence the endpoints, models, and security checks work correctly. Many of these tests are added incrementally alongside each feature story; this story captures the consolidated coverage expectation and gap analysis.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Stub the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Request specs for the three OIDC endpoints covering happy paths and all documented error responses (400, 401, 404, 502).&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt; covering atomic state consumption, replay prevention, expiry, probabilistic cleanup enqueuing, case-insensitive user matching with whitespace normalization, strict &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; handling, and PKCE code verifier flow.&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering YAML loading, ERB interpolation, memoization and reload, missing key detection (warn in dev, raise in production), scope normalization, and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; secrets exclusion.&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;User#generate_jwt&amp;lt;/code&amp;gt; covering payload structure, default and custom expiry, and rejection of tampered tokens.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; specs still pass unchanged.&lt;br /&gt;
&lt;br /&gt;
=== Story 14: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' Vitest coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly. Many of these tests are added incrementally alongside each feature story; this story captures the consolidated coverage expectation and gap analysis.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Component tests for &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Renders nothing on empty or failed providers response.&lt;br /&gt;
** Renders SSO button when providers are returned.&lt;br /&gt;
** Opens the modal when the SSO button is pressed.&lt;br /&gt;
** Populates the provider dropdown with configured providers.&lt;br /&gt;
** Requires both username and provider before submit is enabled.&lt;br /&gt;
** Includes provider id and username in the &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; payload.&lt;br /&gt;
** Redirects the browser to the returned authorization URL on success.&lt;br /&gt;
** Does not redirect on failure.&lt;br /&gt;
* Component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Posts code and state to &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
** Stores the session JWT and dispatches auth state on success.&lt;br /&gt;
** Redirects to the dashboard on success.&lt;br /&gt;
** Displays an error alert and redirects to login on backend failure.&lt;br /&gt;
** Handles the IdP &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; query parameter without calling the backend.&lt;br /&gt;
** Redirects to login when code or state are missing.&lt;br /&gt;
** Shows a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Story 15: Backend — Rate Limiting on OIDC Endpoints ===&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC endpoints rate-limited at the application layer, '''so that''' abusive clients cannot fill the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table, hammer the IdP discovery endpoint, or brute-force callback state values.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add the &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; gem to the Gemfile.&lt;br /&gt;
* Configure throttles in &amp;lt;code&amp;gt;config/initializers/rack_attack.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
** &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: throttled per IP (e.g. 10 requests per minute) to prevent table-fill and discovery-spam attacks.&lt;br /&gt;
** &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: throttled per IP (e.g. 20 requests per minute) to slow brute-force state guessing.&lt;br /&gt;
* Throttled requests return HTTP 429 with a JSON body matching the existing error response shape (&amp;lt;code&amp;gt;{ error: &amp;quot;Too many requests&amp;quot; }&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Use the Rails cache (&amp;lt;code&amp;gt;Rails.cache&amp;lt;/code&amp;gt;) as the backing store so no additional infrastructure is required.&lt;br /&gt;
* Document the rationale in the initializer with a brief comment, including the note that gateway-level rate limiting (nginx, Cloudflare) should be added in front for defense in depth in production.&lt;br /&gt;
* Add request specs verifying:&lt;br /&gt;
** A burst of requests above the threshold returns 429.&lt;br /&gt;
** Requests under the threshold succeed normally.&lt;br /&gt;
** Different IPs are throttled independently.&lt;br /&gt;
* Verify existing endpoints (login, etc.) are not affected by the new rules.&lt;br /&gt;
&lt;br /&gt;
== NCSU Google Provider Setup ==&lt;br /&gt;
&lt;br /&gt;
You can find more details about how to set up the Google OIDC Provider at [https://wiki.expertiza.ncsu.edu/index.php?title=Google_OIDC_Setup Google OIDC Setup]&lt;br /&gt;
&lt;br /&gt;
== Demo Video ==&lt;br /&gt;
&lt;br /&gt;
You can view the feature in action as well as edge cases, tests, and swagger by watching the  [https://drive.google.com/file/d/1I8UrZVbHRGVYDu4LCYoYVtCCm946jCZp/view?usp=sharing| Demo Video]&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=168085</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=168085"/>
		<updated>2026-04-26T01:26:32Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Design Patterns */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
&lt;br /&gt;
Expertiza currently authenticates users with its own login page, implemented by the Expertiza application. Expertiza has been used at many campuses, however, and each has their own SSO (single signon) protocol that students and staff use to log into other applications. Supporting these standard protocols at sites where they are in use is more secure for the application, provides a familiar and streamlined login experience, and frees Expertiza from managing credentials for users whose institution already does so. This design introduces [https://openid.net/developers/how-connect-works/ OIDC] login as an additional authentication option alongside the existing username and password login. Both methods will continue to be supported, allowing users to choose their preferred approach.&lt;br /&gt;
&lt;br /&gt;
== Design Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users enter their Expertiza username on the login page and select a provider from a dropdown. The frontend posts the username and provider to the backend, which returns an authorization URL. The user is redirected to the school's OIDC provider, authenticates, and is redirected back to the frontend callback. The callback posts the authorization code and state to the backend to complete login. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Matching ===&lt;br /&gt;
Match the authenticated user by both the Expertiza username (provided before login) and the verified email claim from the ID token. Username is required because email addresses are not unique in Expertiza; multiple accounts may share the same email. Matching is case-insensitive on both fields. The provider must validate email with the &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim and if it is not &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the login is rejected. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, a generic authentication error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
* OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Client credentials (client ID, client secret) are stored in environment variables and injected via ERB.&lt;br /&gt;
* Providers must support OIDC discovery;&lt;br /&gt;
** Their endpoints and JWKS keys are fetched automatically from the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document.&lt;br /&gt;
* The system supports multiple OIDC provider configurations simultaneously.&lt;br /&gt;
* Provider configuration is validated at boot via config/initializers/oidc.rb. In production, a provider with any missing required key raises OidcConfig::InvalidConfiguration, which prevents the application from starting with a misconfigured OIDC provider. In all other environments (development, test), the invalid provider is skipped with a warning logged so that local development and CI are not blocked when OIDC credentials are not configured.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, PKCE code verifier, username, and provider key are stored server-side in an &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and consumed (deleted) on successful callback. A probabilistic inline cleanup removes stale rows on new request creation to keep the table bounded without requiring a scheduled job. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Login ===&lt;br /&gt;
At one point it was suggested that OIDC completely replace password login if configured.  We considered this idea, however, given this feature must support multiple institutions, some of which may want OIDC and others which may not and there is no institution context pre-login at this time, we felt that probably falls under account management and is beyond the scope of this assignment.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted. The OIDC flow is only used to verify the user's identity with an external provider at login time. Once the user is authenticated, Expertiza issues its own session JWT, and all subsequent requests use that local session. The IdP session is independent of the Expertiza session, so logging out of Expertiza (destroying the local session) does not affect the user's session at the IdP, and logging out of the IdP does not affect the user's Expertiza session. RP-initiated logout (ending the IdP session as part of application logout) is out of scope.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
All callback failure modes (invalid state, expired state, replayed state, no matching user, mismatched username or email, failed token verification, unverified email, unknown provider) return a generic &amp;quot;Authentication failed&amp;quot; response with HTTP 401 to avoid leaking information about which specific check failed. Provider communication failures (discovery or token endpoint unreachable) return HTTP 502. Missing required parameters return HTTP 400. Unknown providers on &amp;lt;code&amp;gt;client-select&amp;lt;/code&amp;gt; return HTTP 404.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. State rows are atomically consumed in a database transaction with row-level locking to prevent race conditions on replay. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE. The &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim is required and must be true.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request and model specs stub the identity provider's discovery, token, and JWKS endpoints to exercise the full controller and model logic (state management, token exchange, ID token verification, user matching, case-insensitive lookup, email verification) without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, username input, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC_Flow_Updated.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present when accessed. In production, invalid configuration raises &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to prevent startup with a misconfigured provider. In other environments, invalid providers are skipped with a warning to avoid blocking local development and CI where OIDC may not be fully configured. Validation runs at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt; so issues surface immediately in production.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id and username. Both parameters are required; missing parameters return a 400. Fetch the provider's &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve endpoints and JWKS keys. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, username, and creation timestamp. With a 10% probability per request, enqueue a &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt; to amortize the cost of deleting stale rows without requiring a dedicated scheduler. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that accepts the authorization code and state. Both parameters are required. Atomically look up and destroy the matching &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row by state within a database transaction with row-level locking, rejecting the request if no row is found or if the row is older than 5 minutes. The atomic consume prevents replay. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. The &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim must be explicitly &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;; tokens without it or with a false value are rejected. Match the user by both the stored username and the verified email claim from the ID token using case-insensitive, whitespace-trimmed comparison (emails are not unique in Expertiza, so username disambiguates). If a match is found, issue a session JWT via &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt; — the same method used by the existing password login. Return a generic 401 &amp;quot;Authentication failed&amp;quot; for all verification or matching failures (invalid state, replayed state, token verification failure, unverified email, no matching user, unknown provider) to avoid leaking which specific check failed.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):'''&lt;br /&gt;
** On page load, the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;&lt;br /&gt;
*** If the request fails or returns empty, the component renders nothing, and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
*** If providers are found, an SSO login button is displayed.&lt;br /&gt;
** Once the SSO Button is clicked, a modal displays with a username field and a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider.&lt;br /&gt;
&lt;br /&gt;
[[File:LoginPageWithSSOButton.png|1000px|Login Page with SSO Button]]&lt;br /&gt;
[[File:SSOLoginModal.png|1000px|SSO Login Modal]]&lt;br /&gt;
&lt;br /&gt;
* '''Initiate Login (Step 2):''' Once the user enters their username, selects a provider, and clicks &amp;quot;Continue with SSO&amp;quot;, the form posts to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with both the provider id and username. On success, the browser is redirected to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend, and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, we store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
&lt;br /&gt;
* '''Strategy pattern''' — Each identity provider is defined as a YAML configuration block with its own credentials, issuer, and scopes. &amp;lt;code&amp;gt;OidcRequest.authorization_uri_for!&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; operate on whatever provider key is passed in, with no &amp;lt;code&amp;gt;if provider == &amp;quot;google&amp;quot;&amp;lt;/code&amp;gt; branching. Adding a new institution's SSO requires only a new YAML entry and environment variables — no code changes anywhere.&lt;br /&gt;
&lt;br /&gt;
* '''Facade''' — OidcLoginController: The controller exposes three simple, clean endpoints (providers, client_select, callback) while hiding the underlying complexity: YAML parsing, PKCE generation, database row management, provider discovery, token exchange, JWKS verification, and user matching all happen inside &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
* '''Chain of Responsibility''' — callback error handling: The callback action processes a sequence of steps -&amp;gt; consume state, exchange code, verify token, match user. Each can fail in a different way.&lt;br /&gt;
&lt;br /&gt;
* '''Command''' — CleanupStaleOidcRequestsJob: The work of deleting stale &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; rows is encapsulated as a background job that can be enqueued, retried, or deferred by Active Job. The job itself is a single line (&amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;). This separates the decision of when to clean up (probabilistic, at row creation time) from the act of cleaning up (the job), and means cleanup can be safely moved to a scheduler later with no logic changes.&lt;br /&gt;
&lt;br /&gt;
* '''Null Object (graceful degradation)''' — OidcModal: When &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; fails or returns an empty list, &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; renders nothing at all. The login page behaves exactly as if OIDC were never configured, and the standard username/password form is completely unaffected. This is cleaner than showing a loading spinner or error state, because from the user's perspective there is simply no SSO option instead of a broken one.&lt;br /&gt;
&lt;br /&gt;
* Provider configuration is memoized as class-level state in &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; and cleared via &amp;lt;code&amp;gt;reload!&amp;lt;/code&amp;gt;, ensuring YAML is parsed once per process.&lt;br /&gt;
&lt;br /&gt;
=== Schema (OidcRequest) ===&lt;br /&gt;
The &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || not null, unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; || string || not null || Expertiza username entered before login; used alongside the verified email claim to match an existing user (emails are not unique)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
No foreign keys or associations to other tables. Stale rows are cleaned up probabilistically on new request creation (10% chance to enqueue &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt;), avoiding the need for a dedicated scheduler.&lt;br /&gt;
&lt;br /&gt;
=== Provider Configuration (OidcConfig) ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; model loads OIDC identity provider definitions from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; at boot. Each provider is defined as a keyed entry under &amp;lt;code&amp;gt;providers:&amp;lt;/code&amp;gt;. The top-level key is the provider id used in API requests and stored in the &amp;lt;code&amp;gt;oidc_requests.provider&amp;lt;/code&amp;gt; column. Client credentials are injected from environment variables via ERB to keep secrets out of version control.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Key !! Required !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| ''provider key'' (e.g. &amp;lt;code&amp;gt;google-ncsu&amp;lt;/code&amp;gt;) || yes || Unique identifier for this provider. Sent by the frontend in &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; and stored on the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row. Use a short, URL-safe slug.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt; || yes || Human-readable name shown to users in the login dropdown (e.g. &amp;quot;Google NCSU&amp;quot;).&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt; || yes || The OIDC issuer URL (e.g. &amp;lt;code&amp;gt;https://accounts.google.com&amp;lt;/code&amp;gt;). Used to fetch the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; discovery document, which provides the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Must match the &amp;lt;code&amp;gt;iss&amp;lt;/code&amp;gt; claim in ID tokens issued by this provider.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt; || yes || OAuth client identifier obtained when registering the application with the identity provider. Sent in the authorization request and token exchange. Typically injected via &amp;lt;code&amp;gt;&amp;lt;%= ENV['PROVIDER_CLIENT_ID'] %&amp;gt;&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt; || yes || OAuth client secret obtained during registration. Used to authenticate the backend to the token endpoint. Must be kept secret — always injected via &amp;lt;code&amp;gt;&amp;lt;%= ENV['PROVIDER_CLIENT_SECRET'] %&amp;gt;&amp;lt;/code&amp;gt;, never hardcoded.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt; || yes || The URL the identity provider redirects to after authentication. Must exactly match the value registered with the provider (scheme, host, port, and path). Should point to the frontend callback route (e.g. &amp;lt;code&amp;gt;http://localhost:3000/auth/callback&amp;lt;/code&amp;gt;).&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt; || no || Space-separated OIDC scopes requested from the provider. Defaults to &amp;lt;code&amp;gt;openid email profile&amp;lt;/code&amp;gt; if omitted. The &amp;lt;code&amp;gt;openid&amp;lt;/code&amp;gt; scope is required to receive an ID token; &amp;lt;code&amp;gt;email&amp;lt;/code&amp;gt; is required for account matching.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; exposes &amp;lt;code&amp;gt;find(provider_key)&amp;lt;/code&amp;gt; for internal lookups and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; for the frontend-facing &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; response (which only includes id and display name, never secrets or endpoints). Providers missing any required key are handled based on environment: in production, startup fails with &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to prevent running with misconfigured providers; in other environments, the invalid provider is skipped with a warning so local development and CI are not blocked. Discovery is always used; non-discovery providers are not supported. The configuration is validated once at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
providers:&lt;br /&gt;
  google-ncsu:&lt;br /&gt;
    display_name: Google NCSU&lt;br /&gt;
    issuer: https://accounts.google.com&lt;br /&gt;
    client_id: &amp;lt;%= ENV['GOOG_CLIENT_ID'] %&amp;gt;&lt;br /&gt;
    client_secret: &amp;lt;%= ENV['GOOG_CLIENT_SECRET'] %&amp;gt;&lt;br /&gt;
    redirect_uri: &amp;lt;%= ENV['GOOG_REDIRECT_URI'] %&amp;gt;&lt;br /&gt;
    scopes: openid email profile&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== New Dependencies ==&lt;br /&gt;
&lt;br /&gt;
=== openid_connect ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
=== rack-attack ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; gem ([https://github.com/rack/rack-attack github.com/rack/rack-attack]) was added to provide rate limiting on the OIDC endpoints. Without it, an attacker could spam &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; to fill the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table or repeatedly probe &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; to attempt token guessing.&lt;br /&gt;
&lt;br /&gt;
* '''Throttle abuse before it reaches the application:''' Rack-level middleware rejects abusive clients before any controller code runs, protecting both the database and the IdP discovery endpoints from being hammered.&lt;br /&gt;
* '''Per-IP throttling on OIDC endpoints:''' &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; is rate-limited to prevent table-fill attacks and discovery spam (each request creates a row and triggers an IdP discovery call). &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; is rate-limited to slow brute-force state guessing — even though the state values have 256 bits of entropy and won't realistically be guessed, throttling prevents wasted work and log noise.&lt;br /&gt;
* '''Lightweight and well-established:''' Single gem, no external dependencies (uses Rails cache for storage), maintained by the Rack team, used by tens of thousands of production Rails apps.&lt;br /&gt;
* '''Configuration in code:''' Rules live in &amp;lt;code&amp;gt;config/initializers/rack_attack.rb&amp;lt;/code&amp;gt; and are version-controlled alongside the rest of the auth configuration, rather than living in a gateway or load balancer config.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is that rate limiting at the application layer is best-effort — a sufficiently distributed attack can still overwhelm the app. For production deployment, a gateway-level rate limit (e.g. nginx, Cloudflare) should be added in front of &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; as defense in depth. &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; is documented as the application-layer baseline, not the only line of defense.&lt;br /&gt;
&lt;br /&gt;
== File Diffs ==&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: [https://github.com/expertiza/reimplementation-back-end/pull/335 reimplementation-back-end#335]&lt;br /&gt;
* Frontend: [https://github.com/expertiza/reimplementation-front-end/pull/172 reimplementation-front-end#172]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
 app/controllers/oidc_login_controller.rb         — Thin controller for providers, client_select, and callback actions with centralized error handling&lt;br /&gt;
 app/models/oidc_request.rb                       — ActiveRecord model owning state/nonce/PKCE/username storage, OIDC flow, account matching, and probabilistic stale cleanup&lt;br /&gt;
 app/models/oidc_config.rb                        — YAML config loader with validation (strict in production), scope normalization, and public_list filtering&lt;br /&gt;
 app/models/user.rb                               — Added &amp;lt;code&amp;gt;generate_jwt&amp;lt;/code&amp;gt; method shared with password login&lt;br /&gt;
 app/jobs/cleanup_stale_oidc_requests_job.rb      — ActiveJob that calls &amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;&lt;br /&gt;
 config/oidc_providers.yml                        — Provider configuration (ERB for env var injection)&lt;br /&gt;
 config/initializers/oidc.rb                      — Boot-time config validation&lt;br /&gt;
 config/routes.rb                                 — New routes for the three OIDC endpoints&lt;br /&gt;
 db/migrate/*_create_oidc_requests.rb             — Migration for oidc_requests table&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
 src/components/Modals/OidcModal.tsx         — Modal displaying the SSO button and provider dropdown with username input&lt;br /&gt;
 src/pages/OidcCallback/OidcCallback.tsx     — Callback page handling code exchange and auth state dispatch&lt;br /&gt;
 src/pages/Authentication/Login.tsx          — Existing login page with the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component added below the password form&lt;br /&gt;
 src/App.tsx                                 — Added &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 /auth/callback            → React OidcCallback component (frontend route)&lt;br /&gt;
&lt;br /&gt;
== Tests ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) ===&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_request_spec.rb spec/models/oidc_request_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''.consume_recent_by_state!'''&lt;br /&gt;
* Returns and destroys a recent request matching state&lt;br /&gt;
* Raises RecordNotFound for unknown state&lt;br /&gt;
* Raises RecordNotFound for expired requests and preserves the row&lt;br /&gt;
* Prevents replay by destroying the row on consumption&lt;br /&gt;
&lt;br /&gt;
'''.delete_stale'''&lt;br /&gt;
* Deletes rows older than the validity window and preserves fresh rows&lt;br /&gt;
&lt;br /&gt;
'''Probabilistic cleanup on create'''&lt;br /&gt;
* Enqueues CleanupStaleOidcRequestsJob when rand falls under the threshold&lt;br /&gt;
* Does not enqueue when rand falls above the threshold&lt;br /&gt;
&lt;br /&gt;
'''.authorization_uri_for!'''&lt;br /&gt;
* Creates an oidc_requests row with username and returns provider authorization URI&lt;br /&gt;
* Uses default scopes when provider scopes are missing&lt;br /&gt;
&lt;br /&gt;
'''#verified_email_from_code!'''&lt;br /&gt;
* Exchanges code, verifies token, and returns email when email_verified is true&lt;br /&gt;
* Raises AuthenticationError when email_verified claim is absent&lt;br /&gt;
* Raises AuthenticationError when email_verified is false&lt;br /&gt;
&lt;br /&gt;
'''#authenticate_user!'''&lt;br /&gt;
* Matches user by exact username and email&lt;br /&gt;
* Matches case-insensitively on username&lt;br /&gt;
* Matches case-insensitively on email&lt;br /&gt;
* Matches case-insensitively on both fields&lt;br /&gt;
* Matches user when DB stores values with leading or trailing whitespace&lt;br /&gt;
* Raises AuthenticationError when email is blank&lt;br /&gt;
* Raises AuthenticationError when email matches but username does not&lt;br /&gt;
* Raises AuthenticationError when username matches but email does not&lt;br /&gt;
* Raises AuthenticationError when neither username nor email match&lt;br /&gt;
&lt;br /&gt;
'''.new_client'''&lt;br /&gt;
* Builds an OpenIDConnect::Client with provider credentials and discovery endpoints&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''.providers'''&lt;br /&gt;
* Loads providers from YAML and evaluates ERB env vars&lt;br /&gt;
* Memoizes results until reload! is called&lt;br /&gt;
* Skips providers missing required keys and warns&lt;br /&gt;
* Returns an empty hash when no providers key exists&lt;br /&gt;
* Returns an empty hash when YAML is empty&lt;br /&gt;
* Returns an empty hash when providers key is null&lt;br /&gt;
* Returns an empty hash when the top-level YAML is not a Hash&lt;br /&gt;
* Returns an empty hash when the providers value is not a Hash&lt;br /&gt;
* Supports YAML aliases in provider definitions&lt;br /&gt;
&lt;br /&gt;
'''Production behavior'''&lt;br /&gt;
* Raises InvalidConfiguration when a provider is missing required keys&lt;br /&gt;
* Raises InvalidConfiguration when the top-level YAML is not a Hash&lt;br /&gt;
* Raises InvalidConfiguration when the providers value is not a Hash&lt;br /&gt;
&lt;br /&gt;
'''.find'''&lt;br /&gt;
* Returns a provider config by key&lt;br /&gt;
* Raises ProviderNotFound for unknown provider keys&lt;br /&gt;
&lt;br /&gt;
'''.public_list'''&lt;br /&gt;
* Returns only id and name for each provider, never secrets&lt;br /&gt;
&lt;br /&gt;
'''.scopes_for'''&lt;br /&gt;
* Parses whitespace-delimited scope strings&lt;br /&gt;
* Parses comma-delimited scope strings&lt;br /&gt;
* Parses mixed comma and whitespace delimiters&lt;br /&gt;
* Falls back to default scopes when scopes is nil&lt;br /&gt;
* Falls back to default scopes when scopes key is absent&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/user_spec.rb spec/models/user_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''#generate_jwt'''&lt;br /&gt;
* Encodes the user attributes (id, name, full_name, role, institution_id, exp) into a JWT&lt;br /&gt;
* Defaults to 24 hour expiry&lt;br /&gt;
* Raises an error when the token signature is invalid (tampered token rejected)&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* Returns authorization URL for a valid provider and username&lt;br /&gt;
* Returns 400 when required parameters are missing&lt;br /&gt;
* Returns 404 for unknown provider&lt;br /&gt;
* Returns 502 when provider discovery fails&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — happy path'''&lt;br /&gt;
* Exchanges valid code and state for a session JWT&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — generic 401 &amp;quot;Authentication failed&amp;quot;'''&lt;br /&gt;
* When no user matches the username and email&lt;br /&gt;
* When email matches but username does not&lt;br /&gt;
* When state is invalid or expired&lt;br /&gt;
* When token verification fails&lt;br /&gt;
* When the stored provider no longer exists in config&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — other errors'''&lt;br /&gt;
* Returns 400 when required parameters are missing&lt;br /&gt;
* Returns 502 when provider discovery fails&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) ===&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/Modals/OidcModal.test.tsx src/components/Modals/OidcModal.test.tsx] ====&lt;br /&gt;
&lt;br /&gt;
'''OidcModal Component'''&lt;br /&gt;
* Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* Renders nothing when GET /auth/providers fails&lt;br /&gt;
* Renders SSO button when providers are returned&lt;br /&gt;
* Opens the modal when the SSO button is pressed&lt;br /&gt;
* Populates the provider dropdown with configured providers&lt;br /&gt;
* Disables submit until both username and provider are provided&lt;br /&gt;
* Posts provider id and username to /auth/client-select on submit&lt;br /&gt;
* Redirects the browser to the returned authorization URL on success&lt;br /&gt;
* Does not redirect when client-select fails&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx] ====&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* Stores session JWT and dispatches auth state on success&lt;br /&gt;
* Redirects to dashboard on successful login&lt;br /&gt;
* Displays error alert and redirects to login on backend failure&lt;br /&gt;
* Handles IdP error query parameter without calling the backend&lt;br /&gt;
* Redirects to login when code or state query parameters are missing&lt;br /&gt;
* Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
== Planning ==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/users/johnmweisz/projects/9 Backend project board]&lt;br /&gt;
* [https://github.com/users/johnmweisz/projects/8 Frontend project board]&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML, exposing methods to list providers, look up a provider by key, and normalize scopes.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;.&lt;br /&gt;
* In production, raise &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to block startup with misconfigured providers; in other environments, skip invalid providers with a warning so local development and CI are not blocked.&lt;br /&gt;
* Validate configuration at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt; so issues surface immediately on deploy.&lt;br /&gt;
* Add unit tests for config loading, validation, missing key detection, scope normalization, and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; secrets exclusion.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — OIDC Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, PKCE code verifier, and username, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, not null, unique, indexed), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; (not null), and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt; model with a &amp;lt;code&amp;gt;consume_recent_by_state!&amp;lt;/code&amp;gt; method that atomically finds, locks, and destroys the row in a transaction to prevent replay.&lt;br /&gt;
* Expose a &amp;lt;code&amp;gt;delete_stale&amp;lt;/code&amp;gt; class method that deletes rows older than the validity window.&lt;br /&gt;
* Add unit tests for creation, atomic consumption, expiry, replay prevention, and stale deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider and username, and returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept required &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; params (return 400 on missing params).&lt;br /&gt;
* Look up the provider config and fetch the discovery document.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier and challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; with state, nonce, code_verifier, provider, and username.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Return a 502 if provider discovery fails.&lt;br /&gt;
* Add request specs covering the happy path, missing params, unknown provider, and discovery failure.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept required &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params (return 400 on missing params).&lt;br /&gt;
* Atomically consume the matching &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row by state, rejecting if not found, expired, or already consumed.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, audience (client_id), and nonce.&lt;br /&gt;
* Reject the login if the &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim is not explicitly &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Match an existing user by username (from &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt;) and email (from ID token), case-insensitive with whitespace trimmed on both.&lt;br /&gt;
* On match: issue a session JWT via &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt; and return &amp;lt;code&amp;gt;{ token }&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Return a generic 401 &amp;quot;Authentication failed&amp;quot; for all verification and matching failures to avoid information leakage.&lt;br /&gt;
* Return a 502 if provider discovery fails.&lt;br /&gt;
* Add request specs covering the happy path, missing params, invalid/expired state, replay, token verification failure, username/email mismatch, unverified email, and unknown provider.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — SSO Modal with Username and Provider Selection ===&lt;br /&gt;
'''As a''' user, '''I want''' to enter my username and select a provider on the login page, '''so that''' I can authenticate with my school credentials against the correct Expertiza account.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Display an SSO button when providers are returned.&lt;br /&gt;
* On button press, open a modal with a username text input and a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown populated with the configured providers.&lt;br /&gt;
* Hide or disable the submit action until both username and provider are provided.&lt;br /&gt;
* If the providers request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers, form validation, and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' submitting the SSO form to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On submit, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id and username.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the payload, redirect, and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Unified Session Response ===&lt;br /&gt;
'''As a''' developer, '''I want''' session token generation shared by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (&amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Update &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; to use the shared method without changing its external response shape.&lt;br /&gt;
* Use the shared method in &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;User#generate_jwt&amp;lt;/code&amp;gt; covering payload structure, default and custom expiry, and rejection of tampered tokens.&lt;br /&gt;
* Verify existing password login request specs still pass.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Externalize Hardcoded Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' the frontend API base URL moved to configuration, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the API base URL (currently &amp;lt;code&amp;gt;http://localhost:3002&amp;lt;/code&amp;gt;) to an environment variable (e.g. &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Replace all hardcoded references in &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;Login&amp;lt;/code&amp;gt; components.&lt;br /&gt;
* Document the variable in the README.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
=== Story 11: Backend — Swagger Documentation for OIDC Endpoints ===&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
=== Story 12: Backend — Probabilistic Cleanup of Stale OIDC Requests ===&lt;br /&gt;
'''As a''' developer, '''I want''' stale OIDC request rows cleaned up automatically without a scheduled job, '''so that''' the table does not grow unbounded from abandoned login attempts and no additional infrastructure is required.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt; ActiveJob that calls &amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On &amp;lt;code&amp;gt;after_create&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt;, enqueue the job with a 10% probability (&amp;lt;code&amp;gt;CLEANUP_PROBABILITY&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Use a &amp;lt;code&amp;gt;VALIDITY_WINDOW&amp;lt;/code&amp;gt; constant so the cleanup threshold matches the consumption window.&lt;br /&gt;
* Add tests verifying stale rows are deleted, fresh rows are preserved, and the job is enqueued at the expected probability.&lt;br /&gt;
&lt;br /&gt;
=== Story 13: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' RSpec coverage for the OIDC backend, '''so that''' I have confidence the endpoints, models, and security checks work correctly. Many of these tests are added incrementally alongside each feature story; this story captures the consolidated coverage expectation and gap analysis.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Stub the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Request specs for the three OIDC endpoints covering happy paths and all documented error responses (400, 401, 404, 502).&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt; covering atomic state consumption, replay prevention, expiry, probabilistic cleanup enqueuing, case-insensitive user matching with whitespace normalization, strict &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; handling, and PKCE code verifier flow.&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering YAML loading, ERB interpolation, memoization and reload, missing key detection (warn in dev, raise in production), scope normalization, and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; secrets exclusion.&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;User#generate_jwt&amp;lt;/code&amp;gt; covering payload structure, default and custom expiry, and rejection of tampered tokens.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; specs still pass unchanged.&lt;br /&gt;
&lt;br /&gt;
=== Story 14: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' Vitest coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly. Many of these tests are added incrementally alongside each feature story; this story captures the consolidated coverage expectation and gap analysis.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Component tests for &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Renders nothing on empty or failed providers response.&lt;br /&gt;
** Renders SSO button when providers are returned.&lt;br /&gt;
** Opens the modal when the SSO button is pressed.&lt;br /&gt;
** Populates the provider dropdown with configured providers.&lt;br /&gt;
** Requires both username and provider before submit is enabled.&lt;br /&gt;
** Includes provider id and username in the &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; payload.&lt;br /&gt;
** Redirects the browser to the returned authorization URL on success.&lt;br /&gt;
** Does not redirect on failure.&lt;br /&gt;
* Component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Posts code and state to &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
** Stores the session JWT and dispatches auth state on success.&lt;br /&gt;
** Redirects to the dashboard on success.&lt;br /&gt;
** Displays an error alert and redirects to login on backend failure.&lt;br /&gt;
** Handles the IdP &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; query parameter without calling the backend.&lt;br /&gt;
** Redirects to login when code or state are missing.&lt;br /&gt;
** Shows a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Story 15: Backend — Rate Limiting on OIDC Endpoints ===&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC endpoints rate-limited at the application layer, '''so that''' abusive clients cannot fill the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table, hammer the IdP discovery endpoint, or brute-force callback state values.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add the &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; gem to the Gemfile.&lt;br /&gt;
* Configure throttles in &amp;lt;code&amp;gt;config/initializers/rack_attack.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
** &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: throttled per IP (e.g. 10 requests per minute) to prevent table-fill and discovery-spam attacks.&lt;br /&gt;
** &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: throttled per IP (e.g. 20 requests per minute) to slow brute-force state guessing.&lt;br /&gt;
* Throttled requests return HTTP 429 with a JSON body matching the existing error response shape (&amp;lt;code&amp;gt;{ error: &amp;quot;Too many requests&amp;quot; }&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Use the Rails cache (&amp;lt;code&amp;gt;Rails.cache&amp;lt;/code&amp;gt;) as the backing store so no additional infrastructure is required.&lt;br /&gt;
* Document the rationale in the initializer with a brief comment, including the note that gateway-level rate limiting (nginx, Cloudflare) should be added in front for defense in depth in production.&lt;br /&gt;
* Add request specs verifying:&lt;br /&gt;
** A burst of requests above the threshold returns 429.&lt;br /&gt;
** Requests under the threshold succeed normally.&lt;br /&gt;
** Different IPs are throttled independently.&lt;br /&gt;
* Verify existing endpoints (login, etc.) are not affected by the new rules.&lt;br /&gt;
&lt;br /&gt;
== NCSU Google Provider Setup ==&lt;br /&gt;
&lt;br /&gt;
You can find more details about how to set up the Google OIDC Provider at [https://wiki.expertiza.ncsu.edu/index.php?title=Google_OIDC_Setup Google OIDC Setup]&lt;br /&gt;
&lt;br /&gt;
== Demo Video ==&lt;br /&gt;
&lt;br /&gt;
You can view the feature in action as well as edge cases, tests, and swagger by watching the  [https://drive.google.com/file/d/1I8UrZVbHRGVYDu4LCYoYVtCCm946jCZp/view?usp=sharing| Demo Video]&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=168084</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=168084"/>
		<updated>2026-04-26T01:19:09Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
&lt;br /&gt;
Expertiza currently authenticates users with its own login page, implemented by the Expertiza application. Expertiza has been used at many campuses, however, and each has their own SSO (single signon) protocol that students and staff use to log into other applications. Supporting these standard protocols at sites where they are in use is more secure for the application, provides a familiar and streamlined login experience, and frees Expertiza from managing credentials for users whose institution already does so. This design introduces [https://openid.net/developers/how-connect-works/ OIDC] login as an additional authentication option alongside the existing username and password login. Both methods will continue to be supported, allowing users to choose their preferred approach.&lt;br /&gt;
&lt;br /&gt;
== Design Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users enter their Expertiza username on the login page and select a provider from a dropdown. The frontend posts the username and provider to the backend, which returns an authorization URL. The user is redirected to the school's OIDC provider, authenticates, and is redirected back to the frontend callback. The callback posts the authorization code and state to the backend to complete login. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Matching ===&lt;br /&gt;
Match the authenticated user by both the Expertiza username (provided before login) and the verified email claim from the ID token. Username is required because email addresses are not unique in Expertiza; multiple accounts may share the same email. Matching is case-insensitive on both fields. The provider must validate email with the &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim and if it is not &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;, the login is rejected. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, a generic authentication error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
* OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Client credentials (client ID, client secret) are stored in environment variables and injected via ERB.&lt;br /&gt;
* Providers must support OIDC discovery;&lt;br /&gt;
** Their endpoints and JWKS keys are fetched automatically from the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document.&lt;br /&gt;
* The system supports multiple OIDC provider configurations simultaneously.&lt;br /&gt;
* Provider configuration is validated at boot via config/initializers/oidc.rb. In production, a provider with any missing required key raises OidcConfig::InvalidConfiguration, which prevents the application from starting with a misconfigured OIDC provider. In all other environments (development, test), the invalid provider is skipped with a warning logged so that local development and CI are not blocked when OIDC credentials are not configured.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, PKCE code verifier, username, and provider key are stored server-side in an &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and consumed (deleted) on successful callback. A probabilistic inline cleanup removes stale rows on new request creation to keep the table bounded without requiring a scheduled job. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Login ===&lt;br /&gt;
At one point it was suggested that OIDC completely replace password login if configured.  We considered this idea, however, given this feature must support multiple institutions, some of which may want OIDC and others which may not and there is no institution context pre-login at this time, we felt that probably falls under account management and is beyond the scope of this assignment.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted. The OIDC flow is only used to verify the user's identity with an external provider at login time. Once the user is authenticated, Expertiza issues its own session JWT, and all subsequent requests use that local session. The IdP session is independent of the Expertiza session, so logging out of Expertiza (destroying the local session) does not affect the user's session at the IdP, and logging out of the IdP does not affect the user's Expertiza session. RP-initiated logout (ending the IdP session as part of application logout) is out of scope.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
All callback failure modes (invalid state, expired state, replayed state, no matching user, mismatched username or email, failed token verification, unverified email, unknown provider) return a generic &amp;quot;Authentication failed&amp;quot; response with HTTP 401 to avoid leaking information about which specific check failed. Provider communication failures (discovery or token endpoint unreachable) return HTTP 502. Missing required parameters return HTTP 400. Unknown providers on &amp;lt;code&amp;gt;client-select&amp;lt;/code&amp;gt; return HTTP 404.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. State rows are atomically consumed in a database transaction with row-level locking to prevent race conditions on replay. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE. The &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim is required and must be true.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request and model specs stub the identity provider's discovery, token, and JWKS endpoints to exercise the full controller and model logic (state management, token exchange, ID token verification, user matching, case-insensitive lookup, email verification) without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, username input, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC_Flow_Updated.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present when accessed. In production, invalid configuration raises &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to prevent startup with a misconfigured provider. In other environments, invalid providers are skipped with a warning to avoid blocking local development and CI where OIDC may not be fully configured. Validation runs at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt; so issues surface immediately in production.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id and username. Both parameters are required; missing parameters return a 400. Fetch the provider's &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve endpoints and JWKS keys. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, username, and creation timestamp. With a 10% probability per request, enqueue a &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt; to amortize the cost of deleting stale rows without requiring a dedicated scheduler. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that accepts the authorization code and state. Both parameters are required. Atomically look up and destroy the matching &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row by state within a database transaction with row-level locking, rejecting the request if no row is found or if the row is older than 5 minutes. The atomic consume prevents replay. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. The &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim must be explicitly &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;; tokens without it or with a false value are rejected. Match the user by both the stored username and the verified email claim from the ID token using case-insensitive, whitespace-trimmed comparison (emails are not unique in Expertiza, so username disambiguates). If a match is found, issue a session JWT via &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt; — the same method used by the existing password login. Return a generic 401 &amp;quot;Authentication failed&amp;quot; for all verification or matching failures (invalid state, replayed state, token verification failure, unverified email, no matching user, unknown provider) to avoid leaking which specific check failed.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):'''&lt;br /&gt;
** On page load, the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;&lt;br /&gt;
*** If the request fails or returns empty, the component renders nothing, and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
*** If providers are found, an SSO login button is displayed.&lt;br /&gt;
** Once the SSO Button is clicked, a modal displays with a username field and a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider.&lt;br /&gt;
&lt;br /&gt;
[[File:LoginPageWithSSOButton.png|1000px|Login Page with SSO Button]]&lt;br /&gt;
[[File:SSOLoginModal.png|1000px|SSO Login Modal]]&lt;br /&gt;
&lt;br /&gt;
* '''Initiate Login (Step 2):''' Once the user enters their username, selects a provider, and clicks &amp;quot;Continue with SSO&amp;quot;, the form posts to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with both the provider id and username. On success, the browser is redirected to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend, and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, we store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
&lt;br /&gt;
* '''Strategy pattern''' — Each OIDC provider is defined declaratively in YAML with its own credentials, scopes, and endpoints, while the controller logic remains provider-agnostic. Adding a new identity provider requires only a new configuration block and environment variables, with no code changes.&lt;br /&gt;
&lt;br /&gt;
* Provider configuration is memoized as class-level state in &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; and cleared via &amp;lt;code&amp;gt;reload!&amp;lt;/code&amp;gt;, ensuring YAML is parsed once per process.&lt;br /&gt;
&lt;br /&gt;
=== Schema (OidcRequest) ===&lt;br /&gt;
The &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || not null, unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; || string || not null || Expertiza username entered before login; used alongside the verified email claim to match an existing user (emails are not unique)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
No foreign keys or associations to other tables. Stale rows are cleaned up probabilistically on new request creation (10% chance to enqueue &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt;), avoiding the need for a dedicated scheduler.&lt;br /&gt;
&lt;br /&gt;
=== Provider Configuration (OidcConfig) ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; model loads OIDC identity provider definitions from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; at boot. Each provider is defined as a keyed entry under &amp;lt;code&amp;gt;providers:&amp;lt;/code&amp;gt;. The top-level key is the provider id used in API requests and stored in the &amp;lt;code&amp;gt;oidc_requests.provider&amp;lt;/code&amp;gt; column. Client credentials are injected from environment variables via ERB to keep secrets out of version control.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Key !! Required !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| ''provider key'' (e.g. &amp;lt;code&amp;gt;google-ncsu&amp;lt;/code&amp;gt;) || yes || Unique identifier for this provider. Sent by the frontend in &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; and stored on the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row. Use a short, URL-safe slug.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt; || yes || Human-readable name shown to users in the login dropdown (e.g. &amp;quot;Google NCSU&amp;quot;).&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt; || yes || The OIDC issuer URL (e.g. &amp;lt;code&amp;gt;https://accounts.google.com&amp;lt;/code&amp;gt;). Used to fetch the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; discovery document, which provides the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Must match the &amp;lt;code&amp;gt;iss&amp;lt;/code&amp;gt; claim in ID tokens issued by this provider.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt; || yes || OAuth client identifier obtained when registering the application with the identity provider. Sent in the authorization request and token exchange. Typically injected via &amp;lt;code&amp;gt;&amp;lt;%= ENV['PROVIDER_CLIENT_ID'] %&amp;gt;&amp;lt;/code&amp;gt;.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt; || yes || OAuth client secret obtained during registration. Used to authenticate the backend to the token endpoint. Must be kept secret — always injected via &amp;lt;code&amp;gt;&amp;lt;%= ENV['PROVIDER_CLIENT_SECRET'] %&amp;gt;&amp;lt;/code&amp;gt;, never hardcoded.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt; || yes || The URL the identity provider redirects to after authentication. Must exactly match the value registered with the provider (scheme, host, port, and path). Should point to the frontend callback route (e.g. &amp;lt;code&amp;gt;http://localhost:3000/auth/callback&amp;lt;/code&amp;gt;).&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt; || no || Space-separated OIDC scopes requested from the provider. Defaults to &amp;lt;code&amp;gt;openid email profile&amp;lt;/code&amp;gt; if omitted. The &amp;lt;code&amp;gt;openid&amp;lt;/code&amp;gt; scope is required to receive an ID token; &amp;lt;code&amp;gt;email&amp;lt;/code&amp;gt; is required for account matching.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; exposes &amp;lt;code&amp;gt;find(provider_key)&amp;lt;/code&amp;gt; for internal lookups and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; for the frontend-facing &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; response (which only includes id and display name, never secrets or endpoints). Providers missing any required key are handled based on environment: in production, startup fails with &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to prevent running with misconfigured providers; in other environments, the invalid provider is skipped with a warning so local development and CI are not blocked. Discovery is always used; non-discovery providers are not supported. The configuration is validated once at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
providers:&lt;br /&gt;
  google-ncsu:&lt;br /&gt;
    display_name: Google NCSU&lt;br /&gt;
    issuer: https://accounts.google.com&lt;br /&gt;
    client_id: &amp;lt;%= ENV['GOOG_CLIENT_ID'] %&amp;gt;&lt;br /&gt;
    client_secret: &amp;lt;%= ENV['GOOG_CLIENT_SECRET'] %&amp;gt;&lt;br /&gt;
    redirect_uri: &amp;lt;%= ENV['GOOG_REDIRECT_URI'] %&amp;gt;&lt;br /&gt;
    scopes: openid email profile&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== New Dependencies ==&lt;br /&gt;
&lt;br /&gt;
=== openid_connect ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
=== rack-attack ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; gem ([https://github.com/rack/rack-attack github.com/rack/rack-attack]) was added to provide rate limiting on the OIDC endpoints. Without it, an attacker could spam &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; to fill the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table or repeatedly probe &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; to attempt token guessing.&lt;br /&gt;
&lt;br /&gt;
* '''Throttle abuse before it reaches the application:''' Rack-level middleware rejects abusive clients before any controller code runs, protecting both the database and the IdP discovery endpoints from being hammered.&lt;br /&gt;
* '''Per-IP throttling on OIDC endpoints:''' &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; is rate-limited to prevent table-fill attacks and discovery spam (each request creates a row and triggers an IdP discovery call). &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; is rate-limited to slow brute-force state guessing — even though the state values have 256 bits of entropy and won't realistically be guessed, throttling prevents wasted work and log noise.&lt;br /&gt;
* '''Lightweight and well-established:''' Single gem, no external dependencies (uses Rails cache for storage), maintained by the Rack team, used by tens of thousands of production Rails apps.&lt;br /&gt;
* '''Configuration in code:''' Rules live in &amp;lt;code&amp;gt;config/initializers/rack_attack.rb&amp;lt;/code&amp;gt; and are version-controlled alongside the rest of the auth configuration, rather than living in a gateway or load balancer config.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is that rate limiting at the application layer is best-effort — a sufficiently distributed attack can still overwhelm the app. For production deployment, a gateway-level rate limit (e.g. nginx, Cloudflare) should be added in front of &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; as defense in depth. &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; is documented as the application-layer baseline, not the only line of defense.&lt;br /&gt;
&lt;br /&gt;
== File Diffs ==&lt;br /&gt;
&lt;br /&gt;
=== Pull Requests ===&lt;br /&gt;
* Backend: [https://github.com/expertiza/reimplementation-back-end/pull/335 reimplementation-back-end#335]&lt;br /&gt;
* Frontend: [https://github.com/expertiza/reimplementation-front-end/pull/172 reimplementation-front-end#172]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
 app/controllers/oidc_login_controller.rb         — Thin controller for providers, client_select, and callback actions with centralized error handling&lt;br /&gt;
 app/models/oidc_request.rb                       — ActiveRecord model owning state/nonce/PKCE/username storage, OIDC flow, account matching, and probabilistic stale cleanup&lt;br /&gt;
 app/models/oidc_config.rb                        — YAML config loader with validation (strict in production), scope normalization, and public_list filtering&lt;br /&gt;
 app/models/user.rb                               — Added &amp;lt;code&amp;gt;generate_jwt&amp;lt;/code&amp;gt; method shared with password login&lt;br /&gt;
 app/jobs/cleanup_stale_oidc_requests_job.rb      — ActiveJob that calls &amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;&lt;br /&gt;
 config/oidc_providers.yml                        — Provider configuration (ERB for env var injection)&lt;br /&gt;
 config/initializers/oidc.rb                      — Boot-time config validation&lt;br /&gt;
 config/routes.rb                                 — New routes for the three OIDC endpoints&lt;br /&gt;
 db/migrate/*_create_oidc_requests.rb             — Migration for oidc_requests table&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
 src/components/Modals/OidcModal.tsx         — Modal displaying the SSO button and provider dropdown with username input&lt;br /&gt;
 src/pages/OidcCallback/OidcCallback.tsx     — Callback page handling code exchange and auth state dispatch&lt;br /&gt;
 src/pages/Authentication/Login.tsx          — Existing login page with the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component added below the password form&lt;br /&gt;
 src/App.tsx                                 — Added &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 /auth/callback            → React OidcCallback component (frontend route)&lt;br /&gt;
&lt;br /&gt;
== Tests ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) ===&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_request_spec.rb spec/models/oidc_request_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''.consume_recent_by_state!'''&lt;br /&gt;
* Returns and destroys a recent request matching state&lt;br /&gt;
* Raises RecordNotFound for unknown state&lt;br /&gt;
* Raises RecordNotFound for expired requests and preserves the row&lt;br /&gt;
* Prevents replay by destroying the row on consumption&lt;br /&gt;
&lt;br /&gt;
'''.delete_stale'''&lt;br /&gt;
* Deletes rows older than the validity window and preserves fresh rows&lt;br /&gt;
&lt;br /&gt;
'''Probabilistic cleanup on create'''&lt;br /&gt;
* Enqueues CleanupStaleOidcRequestsJob when rand falls under the threshold&lt;br /&gt;
* Does not enqueue when rand falls above the threshold&lt;br /&gt;
&lt;br /&gt;
'''.authorization_uri_for!'''&lt;br /&gt;
* Creates an oidc_requests row with username and returns provider authorization URI&lt;br /&gt;
* Uses default scopes when provider scopes are missing&lt;br /&gt;
&lt;br /&gt;
'''#verified_email_from_code!'''&lt;br /&gt;
* Exchanges code, verifies token, and returns email when email_verified is true&lt;br /&gt;
* Raises AuthenticationError when email_verified claim is absent&lt;br /&gt;
* Raises AuthenticationError when email_verified is false&lt;br /&gt;
&lt;br /&gt;
'''#authenticate_user!'''&lt;br /&gt;
* Matches user by exact username and email&lt;br /&gt;
* Matches case-insensitively on username&lt;br /&gt;
* Matches case-insensitively on email&lt;br /&gt;
* Matches case-insensitively on both fields&lt;br /&gt;
* Matches user when DB stores values with leading or trailing whitespace&lt;br /&gt;
* Raises AuthenticationError when email is blank&lt;br /&gt;
* Raises AuthenticationError when email matches but username does not&lt;br /&gt;
* Raises AuthenticationError when username matches but email does not&lt;br /&gt;
* Raises AuthenticationError when neither username nor email match&lt;br /&gt;
&lt;br /&gt;
'''.new_client'''&lt;br /&gt;
* Builds an OpenIDConnect::Client with provider credentials and discovery endpoints&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''.providers'''&lt;br /&gt;
* Loads providers from YAML and evaluates ERB env vars&lt;br /&gt;
* Memoizes results until reload! is called&lt;br /&gt;
* Skips providers missing required keys and warns&lt;br /&gt;
* Returns an empty hash when no providers key exists&lt;br /&gt;
* Returns an empty hash when YAML is empty&lt;br /&gt;
* Returns an empty hash when providers key is null&lt;br /&gt;
* Returns an empty hash when the top-level YAML is not a Hash&lt;br /&gt;
* Returns an empty hash when the providers value is not a Hash&lt;br /&gt;
* Supports YAML aliases in provider definitions&lt;br /&gt;
&lt;br /&gt;
'''Production behavior'''&lt;br /&gt;
* Raises InvalidConfiguration when a provider is missing required keys&lt;br /&gt;
* Raises InvalidConfiguration when the top-level YAML is not a Hash&lt;br /&gt;
* Raises InvalidConfiguration when the providers value is not a Hash&lt;br /&gt;
&lt;br /&gt;
'''.find'''&lt;br /&gt;
* Returns a provider config by key&lt;br /&gt;
* Raises ProviderNotFound for unknown provider keys&lt;br /&gt;
&lt;br /&gt;
'''.public_list'''&lt;br /&gt;
* Returns only id and name for each provider, never secrets&lt;br /&gt;
&lt;br /&gt;
'''.scopes_for'''&lt;br /&gt;
* Parses whitespace-delimited scope strings&lt;br /&gt;
* Parses comma-delimited scope strings&lt;br /&gt;
* Parses mixed comma and whitespace delimiters&lt;br /&gt;
* Falls back to default scopes when scopes is nil&lt;br /&gt;
* Falls back to default scopes when scopes key is absent&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/user_spec.rb spec/models/user_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''#generate_jwt'''&lt;br /&gt;
* Encodes the user attributes (id, name, full_name, role, institution_id, exp) into a JWT&lt;br /&gt;
* Defaults to 24 hour expiry&lt;br /&gt;
* Raises an error when the token signature is invalid (tampered token rejected)&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb] ====&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* Returns authorization URL for a valid provider and username&lt;br /&gt;
* Returns 400 when required parameters are missing&lt;br /&gt;
* Returns 404 for unknown provider&lt;br /&gt;
* Returns 502 when provider discovery fails&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — happy path'''&lt;br /&gt;
* Exchanges valid code and state for a session JWT&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — generic 401 &amp;quot;Authentication failed&amp;quot;'''&lt;br /&gt;
* When no user matches the username and email&lt;br /&gt;
* When email matches but username does not&lt;br /&gt;
* When state is invalid or expired&lt;br /&gt;
* When token verification fails&lt;br /&gt;
* When the stored provider no longer exists in config&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — other errors'''&lt;br /&gt;
* Returns 400 when required parameters are missing&lt;br /&gt;
* Returns 502 when provider discovery fails&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) ===&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/Modals/OidcModal.test.tsx src/components/Modals/OidcModal.test.tsx] ====&lt;br /&gt;
&lt;br /&gt;
'''OidcModal Component'''&lt;br /&gt;
* Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* Renders nothing when GET /auth/providers fails&lt;br /&gt;
* Renders SSO button when providers are returned&lt;br /&gt;
* Opens the modal when the SSO button is pressed&lt;br /&gt;
* Populates the provider dropdown with configured providers&lt;br /&gt;
* Disables submit until both username and provider are provided&lt;br /&gt;
* Posts provider id and username to /auth/client-select on submit&lt;br /&gt;
* Redirects the browser to the returned authorization URL on success&lt;br /&gt;
* Does not redirect when client-select fails&lt;br /&gt;
&lt;br /&gt;
==== [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx] ====&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* Stores session JWT and dispatches auth state on success&lt;br /&gt;
* Redirects to dashboard on successful login&lt;br /&gt;
* Displays error alert and redirects to login on backend failure&lt;br /&gt;
* Handles IdP error query parameter without calling the backend&lt;br /&gt;
* Redirects to login when code or state query parameters are missing&lt;br /&gt;
* Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
== Planning ==&lt;br /&gt;
&lt;br /&gt;
* [https://github.com/users/johnmweisz/projects/9 Backend project board]&lt;br /&gt;
* [https://github.com/users/johnmweisz/projects/8 Frontend project board]&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML, exposing methods to list providers, look up a provider by key, and normalize scopes.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;.&lt;br /&gt;
* In production, raise &amp;lt;code&amp;gt;OidcConfig::InvalidConfiguration&amp;lt;/code&amp;gt; to block startup with misconfigured providers; in other environments, skip invalid providers with a warning so local development and CI are not blocked.&lt;br /&gt;
* Validate configuration at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt; so issues surface immediately on deploy.&lt;br /&gt;
* Add unit tests for config loading, validation, missing key detection, scope normalization, and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; secrets exclusion.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — OIDC Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, PKCE code verifier, and username, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, not null, unique, indexed), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; (not null), &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; (not null), and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt; model with a &amp;lt;code&amp;gt;consume_recent_by_state!&amp;lt;/code&amp;gt; method that atomically finds, locks, and destroys the row in a transaction to prevent replay.&lt;br /&gt;
* Expose a &amp;lt;code&amp;gt;delete_stale&amp;lt;/code&amp;gt; class method that deletes rows older than the validity window.&lt;br /&gt;
* Add unit tests for creation, atomic consumption, expiry, replay prevention, and stale deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider and username, and returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept required &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;username&amp;lt;/code&amp;gt; params (return 400 on missing params).&lt;br /&gt;
* Look up the provider config and fetch the discovery document.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier and challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; with state, nonce, code_verifier, provider, and username.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Return a 502 if provider discovery fails.&lt;br /&gt;
* Add request specs covering the happy path, missing params, unknown provider, and discovery failure.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept required &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params (return 400 on missing params).&lt;br /&gt;
* Atomically consume the matching &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; row by state, rejecting if not found, expired, or already consumed.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, audience (client_id), and nonce.&lt;br /&gt;
* Reject the login if the &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; claim is not explicitly &amp;lt;code&amp;gt;true&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Match an existing user by username (from &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt;) and email (from ID token), case-insensitive with whitespace trimmed on both.&lt;br /&gt;
* On match: issue a session JWT via &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt; and return &amp;lt;code&amp;gt;{ token }&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Return a generic 401 &amp;quot;Authentication failed&amp;quot; for all verification and matching failures to avoid information leakage.&lt;br /&gt;
* Return a 502 if provider discovery fails.&lt;br /&gt;
* Add request specs covering the happy path, missing params, invalid/expired state, replay, token verification failure, username/email mismatch, unverified email, and unknown provider.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — SSO Modal with Username and Provider Selection ===&lt;br /&gt;
'''As a''' user, '''I want''' to enter my username and select a provider on the login page, '''so that''' I can authenticate with my school credentials against the correct Expertiza account.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Display an SSO button when providers are returned.&lt;br /&gt;
* On button press, open a modal with a username text input and a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown populated with the configured providers.&lt;br /&gt;
* Hide or disable the submit action until both username and provider are provided.&lt;br /&gt;
* If the providers request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers, form validation, and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' submitting the SSO form to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On submit, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id and username.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the payload, redirect, and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Unified Session Response ===&lt;br /&gt;
'''As a''' developer, '''I want''' session token generation shared by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (&amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Update &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; to use the shared method without changing its external response shape.&lt;br /&gt;
* Use the shared method in &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;User#generate_jwt&amp;lt;/code&amp;gt; covering payload structure, default and custom expiry, and rejection of tampered tokens.&lt;br /&gt;
* Verify existing password login request specs still pass.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Externalize Hardcoded Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' the frontend API base URL moved to configuration, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the API base URL (currently &amp;lt;code&amp;gt;http://localhost:3002&amp;lt;/code&amp;gt;) to an environment variable (e.g. &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Replace all hardcoded references in &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;Login&amp;lt;/code&amp;gt; components.&lt;br /&gt;
* Document the variable in the README.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
=== Story 11: Backend — Swagger Documentation for OIDC Endpoints ===&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
=== Story 12: Backend — Probabilistic Cleanup of Stale OIDC Requests ===&lt;br /&gt;
'''As a''' developer, '''I want''' stale OIDC request rows cleaned up automatically without a scheduled job, '''so that''' the table does not grow unbounded from abandoned login attempts and no additional infrastructure is required.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;CleanupStaleOidcRequestsJob&amp;lt;/code&amp;gt; ActiveJob that calls &amp;lt;code&amp;gt;OidcRequest.delete_stale&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On &amp;lt;code&amp;gt;after_create&amp;lt;/code&amp;gt; of &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt;, enqueue the job with a 10% probability (&amp;lt;code&amp;gt;CLEANUP_PROBABILITY&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Use a &amp;lt;code&amp;gt;VALIDITY_WINDOW&amp;lt;/code&amp;gt; constant so the cleanup threshold matches the consumption window.&lt;br /&gt;
* Add tests verifying stale rows are deleted, fresh rows are preserved, and the job is enqueued at the expected probability.&lt;br /&gt;
&lt;br /&gt;
=== Story 13: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' RSpec coverage for the OIDC backend, '''so that''' I have confidence the endpoints, models, and security checks work correctly. Many of these tests are added incrementally alongside each feature story; this story captures the consolidated coverage expectation and gap analysis.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Stub the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Request specs for the three OIDC endpoints covering happy paths and all documented error responses (400, 401, 404, 502).&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;OidcRequest&amp;lt;/code&amp;gt; covering atomic state consumption, replay prevention, expiry, probabilistic cleanup enqueuing, case-insensitive user matching with whitespace normalization, strict &amp;lt;code&amp;gt;email_verified&amp;lt;/code&amp;gt; handling, and PKCE code verifier flow.&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering YAML loading, ERB interpolation, memoization and reload, missing key detection (warn in dev, raise in production), scope normalization, and &amp;lt;code&amp;gt;public_list&amp;lt;/code&amp;gt; secrets exclusion.&lt;br /&gt;
* Model specs for &amp;lt;code&amp;gt;User#generate_jwt&amp;lt;/code&amp;gt; covering payload structure, default and custom expiry, and rejection of tampered tokens.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; specs still pass unchanged.&lt;br /&gt;
&lt;br /&gt;
=== Story 14: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' Vitest coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly. Many of these tests are added incrementally alongside each feature story; this story captures the consolidated coverage expectation and gap analysis.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Component tests for &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Renders nothing on empty or failed providers response.&lt;br /&gt;
** Renders SSO button when providers are returned.&lt;br /&gt;
** Opens the modal when the SSO button is pressed.&lt;br /&gt;
** Populates the provider dropdown with configured providers.&lt;br /&gt;
** Requires both username and provider before submit is enabled.&lt;br /&gt;
** Includes provider id and username in the &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; payload.&lt;br /&gt;
** Redirects the browser to the returned authorization URL on success.&lt;br /&gt;
** Does not redirect on failure.&lt;br /&gt;
* Component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Posts code and state to &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
** Stores the session JWT and dispatches auth state on success.&lt;br /&gt;
** Redirects to the dashboard on success.&lt;br /&gt;
** Displays an error alert and redirects to login on backend failure.&lt;br /&gt;
** Handles the IdP &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; query parameter without calling the backend.&lt;br /&gt;
** Redirects to login when code or state are missing.&lt;br /&gt;
** Shows a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcModal&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Story 15: Backend — Rate Limiting on OIDC Endpoints ===&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC endpoints rate-limited at the application layer, '''so that''' abusive clients cannot fill the &amp;lt;code&amp;gt;oidc_requests&amp;lt;/code&amp;gt; table, hammer the IdP discovery endpoint, or brute-force callback state values.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add the &amp;lt;code&amp;gt;rack-attack&amp;lt;/code&amp;gt; gem to the Gemfile.&lt;br /&gt;
* Configure throttles in &amp;lt;code&amp;gt;config/initializers/rack_attack.rb&amp;lt;/code&amp;gt;:&lt;br /&gt;
** &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;: throttled per IP (e.g. 10 requests per minute) to prevent table-fill and discovery-spam attacks.&lt;br /&gt;
** &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;: throttled per IP (e.g. 20 requests per minute) to slow brute-force state guessing.&lt;br /&gt;
* Throttled requests return HTTP 429 with a JSON body matching the existing error response shape (&amp;lt;code&amp;gt;{ error: &amp;quot;Too many requests&amp;quot; }&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Use the Rails cache (&amp;lt;code&amp;gt;Rails.cache&amp;lt;/code&amp;gt;) as the backing store so no additional infrastructure is required.&lt;br /&gt;
* Document the rationale in the initializer with a brief comment, including the note that gateway-level rate limiting (nginx, Cloudflare) should be added in front for defense in depth in production.&lt;br /&gt;
* Add request specs verifying:&lt;br /&gt;
** A burst of requests above the threshold returns 429.&lt;br /&gt;
** Requests under the threshold succeed normally.&lt;br /&gt;
** Different IPs are throttled independently.&lt;br /&gt;
* Verify existing endpoints (login, etc.) are not affected by the new rules.&lt;br /&gt;
&lt;br /&gt;
== NCSU Google Provider Setup ==&lt;br /&gt;
&lt;br /&gt;
You can find more details about how to set up the Google OIDC Provider at [https://wiki.expertiza.ncsu.edu/index.php?title=Google_OIDC_Setup Google OIDC Setup]&lt;br /&gt;
&lt;br /&gt;
== Demo Video ==&lt;br /&gt;
&lt;br /&gt;
You can view the feature in action as well as edge cases, tests, and swagger by watching the  [https://drive.google.com/file/d/1I8UrZVbHRGVYDu4LCYoYVtCCm946jCZp/view?usp=sharing| Demo Video]&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167957</id>
		<title>CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2618._Support_OIDC_Logins&amp;diff=167957"/>
		<updated>2026-04-14T21:40:06Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Purpose */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Purpose ==&lt;br /&gt;
By integrating [https://openid.net/developers/how-connect-works/ OIDC] login, users can authenticate using their existing university credentials, providing a familiar and streamlined login experience. Traditional username and password login will continue to be supported alongside OIDC, allowing users to choose their preferred authentication method.&lt;br /&gt;
&lt;br /&gt;
== Requirements ==&lt;br /&gt;
=== Authentication Flow ===&lt;br /&gt;
Users select a provider from a dropdown on the login page that redirects them to the school's OIDC provider, authenticates them, and redirects back to the application with a valid session. The frontend fetches available providers from the backend via &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders them dynamically in a dropdown. On provider selection, the frontend posts to the backend, which returns an authorization URL. After the user completes the OIDC flow, the IdP redirects back to the frontend callback, which posts the authorization code and state to the backend to complete login.&lt;br /&gt;
&lt;br /&gt;
=== Session Management ===&lt;br /&gt;
Issue and maintain a local application session (JWT) after successful OIDC authentication, using the same &amp;lt;code&amp;gt;JsonWebToken&amp;lt;/code&amp;gt; class and payload structure as the existing password login. Refresh token grant flow will not be considered at this time (since session is managed by the application).&lt;br /&gt;
&lt;br /&gt;
=== Account Linking ===&lt;br /&gt;
Match the authenticated user's email from the ID token to an existing local account. No dedicated account linking table or just-in-time account creation will be built at this time. If no matching local account is found, an error is returned.&lt;br /&gt;
&lt;br /&gt;
=== Configuration ===&lt;br /&gt;
OIDC provider configurations (display name, scopes, endpoints) are defined in a YAML config file (&amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt;). Client credentials (client ID, client secret) are stored in environment variables and injected via ERB. Providers that support OIDC discovery have their endpoints and JWKS keys fetched automatically. The system supports multiple OIDC provider configurations simultaneously. The configuration is validated at boot via an initializer, surfacing missing values immediately on deploy rather than at login time.&lt;br /&gt;
&lt;br /&gt;
=== State Management ===&lt;br /&gt;
OIDC state, nonce, and PKCE code verifier are stored server-side in an &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; database table (via ActiveRecord) rather than in session cookies. This avoids cross-origin cookie issues between the separate frontend and backend. Rows are expired after 5 minutes and deleted after use. This table is named generically to support future federated auth protocols such as SAML. Note that many OIDC libraries (including &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt;) use cookies to track state; due to SameSite restrictions on cross-origin requests, this approach leads to instability with a separated frontend and backend and should be avoided.&lt;br /&gt;
&lt;br /&gt;
=== Logout ===&lt;br /&gt;
Logout will not be impacted.&lt;br /&gt;
&lt;br /&gt;
=== Error Handling ===&lt;br /&gt;
Gracefully handle failures such as no local account matching the authenticated email, expired or invalid state parameters, token exchange errors, ID token verification errors, and user-denied consent at the IdP.&lt;br /&gt;
&lt;br /&gt;
=== Security ===&lt;br /&gt;
Use the Authorization Code flow with the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; Ruby gem (by nov). Validate the ID token signature and claims via JWKS keys from the provider's discovery document. Enforce a &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; parameter to prevent CSRF and a &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; to prevent replay attacks. PKCE (code verifier and code challenge) is always included in the authorization request and token exchange; providers that support it will enforce it, and providers that do not will ignore the extra parameters. The backend is a confidential client and always authenticates with both client secret and PKCE.&lt;br /&gt;
&lt;br /&gt;
=== Testing ===&lt;br /&gt;
Backend and frontend are tested independently. Backend request specs use WebMock to stub the identity provider's discovery, token, and JWKS endpoints, allowing the full controller logic (state management, token exchange, ID token verification, user matching) to be tested without external dependencies. Frontend component tests mock axios calls to verify rendering, dropdown behavior, callback handling, and error display. End-to-end testing across both systems with a live identity provider is not planned at this time, as it would require standing up a mock IdP server (e.g. Keycloak or mock-oauth2-server), which is beyond the scope of the existing test infrastructure. The full OIDC login flow will be manually verified against Google's OIDC provider in a local development environment and demonstrated as needed.&lt;br /&gt;
&lt;br /&gt;
== Design ==&lt;br /&gt;
&lt;br /&gt;
[[File:OIDC Provider-2026-04-06-223511.png|1000px]]&lt;br /&gt;
&lt;br /&gt;
=== Backend ===&lt;br /&gt;
* '''Boot (Step 0):''' Load provider configurations from &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with secrets injected from environment variables via ERB. Each provider entry defines a display name, scopes, issuer, client credentials, and redirect URI. The &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class validates that all required keys are present at boot via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;. For providers with &amp;lt;code&amp;gt;discovery: true&amp;lt;/code&amp;gt;, the &amp;lt;code&amp;gt;.well-known/openid-configuration&amp;lt;/code&amp;gt; document is fetched using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem to resolve the authorization endpoint, token endpoint, userinfo endpoint, and JWKS keys. Discovery results are not aggressively cached to allow for key rotation; on signature verification failure, keys are re-fetched and verification is retried once.&lt;br /&gt;
* '''Provider List (Step 1):''' Expose a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;. No secrets or endpoint details are included in this response.&lt;br /&gt;
* '''Client Select (Step 2):''' Expose a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that accepts a provider id. Generate a cryptographically random state and nonce via &amp;lt;code&amp;gt;SecureRandom.hex(32)&amp;lt;/code&amp;gt;, and a PKCE code verifier via &amp;lt;code&amp;gt;SecureRandom.urlsafe_base64(64)&amp;lt;/code&amp;gt; with a SHA256 code challenge. Insert a row into the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table containing the state, nonce, code verifier, provider id, and creation timestamp. Construct the authorization URL using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem's &amp;lt;code&amp;gt;authorization_uri&amp;lt;/code&amp;gt; method and return it to the frontend.&lt;br /&gt;
* '''Callback (Step 4):''' Expose a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint (and a temporary &amp;lt;code&amp;gt;GET&amp;lt;/code&amp;gt; for direct IdP redirect during backend-only testing) that accepts the authorization code and state. Look up the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row by state, rejecting the request if no row is found or if the row is older than 5 minutes. Delete the row to prevent reuse. Using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem, exchange the authorization code for tokens via &amp;lt;code&amp;gt;access_token!&amp;lt;/code&amp;gt; with the stored code verifier. Decode the ID token using &amp;lt;code&amp;gt;OpenIDConnect::ResponseObject::IdToken.decode&amp;lt;/code&amp;gt; against the provider's JWKS keys, and verify the issuer, client_id, and nonce via &amp;lt;code&amp;gt;id_token.verify!&amp;lt;/code&amp;gt;. Extract the user's email from the ID token claims and look up a matching local user. If a match is found, issue a session JWT using the same &amp;lt;code&amp;gt;JsonWebToken.encode&amp;lt;/code&amp;gt; method and payload structure as the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; action. If no match is found, return a 404 error indicating no local account exists for that email.&lt;br /&gt;
&lt;br /&gt;
=== Frontend ===&lt;br /&gt;
* '''Login Page (Step 1):''' On page load, the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; and renders a dropdown (&amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt;) for each configured provider below the existing username and password form. If the request fails or returns empty, the component renders nothing and the standard login form remains available and unaffected. No loading state is shown to avoid visual disruption when no providers are configured.&lt;br /&gt;
* '''Initiate Login (Step 2):''' When the user selects a provider from the dropdown, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the selected provider id. On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;. The user then authenticates with the identity provider and is redirected back to the frontend callback route.&lt;br /&gt;
* '''Callback (Step 4):''' The &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; page component handles the redirect back from the identity provider at &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. It extracts the authorization code and state from the query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt;s them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;. If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; parameter instead of a code (e.g. the user denied consent), the error is displayed without calling the backend and the user is redirected to the login page.&lt;br /&gt;
* '''Login Complete (Step 5):''' On a successful callback response, store the session JWT via &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, update the Redux auth state via &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, persist the session to localStorage, and redirect the user to the dashboard. This mirrors the existing password login flow exactly. On failure, display an error alert and redirect to the login page.&lt;br /&gt;
* The existing username and password login flow remains unchanged and fully functional.&lt;br /&gt;
&lt;br /&gt;
=== Design Patterns ===&lt;br /&gt;
The implementation uses the '''Strategy pattern''' for provider configuration. Each OIDC provider is defined declaratively in YAML with its own credentials, scopes, and endpoints, while the controller logic remains provider-agnostic. Adding a new identity provider requires only a new configuration block and environment variables, with no code changes.&lt;br /&gt;
&lt;br /&gt;
=== Schema ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table stores temporary OIDC login state. Each row represents a single in-progress login attempt and is deleted after use or expiry.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Column !! Type !! Constraints !! Purpose&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;id&amp;lt;/code&amp;gt; || bigint || primary key || Row identifier&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; || string || unique, indexed || CSRF protection; used to look up the request on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt; || string || not null || Replay attack prevention; verified against the ID token claim&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt; || string || not null || PKCE secret; sent to the token endpoint to prove the same party initiated the flow&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; || string || not null || Which OIDC provider config to use on callback&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; || datetime || not null || Used to expire rows older than 5 minutes&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
No foreign keys or associations to other tables. This table is intentionally generic to support future federated auth protocols such as SAML.&lt;br /&gt;
&lt;br /&gt;
== Library Choice ==&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem (by nov, [https://github.com/nov/openid_connect github.com/nov/openid_connect]) was chosen over &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; for the following reasons:&lt;br /&gt;
&lt;br /&gt;
* '''No cookie/session dependency:''' &amp;lt;code&amp;gt;omniauth_openid_connect&amp;lt;/code&amp;gt; stores state and nonce in the server-side session via cookies. With a separate frontend and backend on different origins, session cookies are not reliably shared due to SameSite restrictions. Using &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; directly allows state management via the database instead.&lt;br /&gt;
* '''Explicit control:''' The gem provides building blocks (discovery, client construction, token exchange, ID token verification) without middleware magic. Each step in the OIDC flow is visible in the controller code.&lt;br /&gt;
* '''Lightweight:''' No OmniAuth middleware stack or Rack integration required. The gem handles the protocol; the application handles routing and state.&lt;br /&gt;
* '''Actively maintained:''' The gem is OpenID Foundation certified and used by 2,700+ projects on GitHub.&lt;br /&gt;
&lt;br /&gt;
The tradeoff is approximately 10 additional lines of code for state management (generating and storing state/nonce/PKCE in the &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; table), which is minimal compared to the complexity of debugging cross-origin cookie issues.&lt;br /&gt;
&lt;br /&gt;
== File Diffs and Additions (Prototype and subject to change) ==&lt;br /&gt;
&lt;br /&gt;
=== Backend (Rails) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/controllers/oidc_login_controller.rb app/controllers/oidc_login_controller.rb]  — Controller with providers, client_select, and callback actions&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/oidc_config.rb app/models/oidc_config.rb]                 — YAML config loader with validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/app/models/auth_request.rb app/models/auth_request.rb]                — ActiveRecord model for state/nonce/PKCE storage&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/oidc_providers.yml config/oidc_providers.yml]                 — Provider configuration (ERB for env var injection)&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/config/initializers/oidc.rb config/initializers/oidc.rb]               — Boot-time config validation&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/db/migrate/20260407003623_create_auth_requests.rb db/migrate/*_create_auth_requests.rb]      — Migration for auth_requests table&lt;br /&gt;
&lt;br /&gt;
=== Backend (RSpec) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-back-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/oidc_config_spec.rb spec/models/oidc_config_spec.rb]          — Config loading, validation, missing keys, public_list secrets exclusion, provider lookup&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/models/auth_request_spec.rb spec/models/auth_request_spec.rb]         — State uniqueness, expiry scope, cleanup of stale rows&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-back-end/blob/2618-oidc-login/spec/requests/oidc_login_spec.rb spec/requests/oidc_login_spec.rb]         — Endpoint tests covering:&lt;br /&gt;
&lt;br /&gt;
'''GET /auth/providers'''&lt;br /&gt;
* Returns provider list with id and name only, no secrets leaked&lt;br /&gt;
* '''TODO''' Returns empty array when no providers configured&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/client-select'''&lt;br /&gt;
* '''TODO''' Returns authorization URL with expected query parameters (client_id, redirect_uri, scope, state, nonce, code_challenge)&lt;br /&gt;
* '''TODO''' Creates an auth_requests row&lt;br /&gt;
* '''TODO''' Returns 404 for unknown provider&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Happy Path'''&lt;br /&gt;
* '''TODO''' Exchanges valid code and state for a session JWT with same payload structure as password login&lt;br /&gt;
* '''TODO''' Deletes the auth_requests row after successful use&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — CSRF Protection (State Validation)'''&lt;br /&gt;
* '''TODO''' Rejects unknown state (422)&lt;br /&gt;
* '''TODO''' Rejects expired state older than 5 minutes (422)&lt;br /&gt;
* '''TODO''' Rejects already-consumed state / replay (422)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Replay Protection (Nonce Validation)'''&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched nonce&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — Token Verification'''&lt;br /&gt;
* '''TODO''' Rejects ID token with invalid signature&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched issuer&lt;br /&gt;
* '''TODO''' Rejects ID token with mismatched audience (client_id)&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — User Matching'''&lt;br /&gt;
* '''TODO''' Returns 404 when no local user matches the email&lt;br /&gt;
* '''TODO''' Matches correct user when multiple users exist&lt;br /&gt;
&lt;br /&gt;
'''POST /auth/callback — PKCE'''&lt;br /&gt;
* '''TODO''' Sends code_verifier to the token endpoint during code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (React) ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.tsx src/components/OidcLogin/OidcLogin.tsx]     — Provider dropdown component&lt;br /&gt;
 [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.tsx src/pages/OidcCallback/OidcCallback.tsx]    — Callback page handling code exchange&lt;br /&gt;
&lt;br /&gt;
=== Frontend (Vitest) Tests ===&lt;br /&gt;
([https://github.com/johnmweisz/reimplementation-front-end/tree/2618-oidc-login branch: 2618-oidc-login])&lt;br /&gt;
&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/components/OidcLogin/OidcLogin.test.tsx src/components/OidcLogin/OidcLogin.test.tsx]     — Provider dropdown component tests&lt;br /&gt;
 '''TODO''' [https://github.com/johnmweisz/reimplementation-front-end/blob/2618-oidc-login/src/pages/OidcCallback/OidcCallback.test.tsx src/pages/OidcCallback/OidcCallback.test.tsx]    — Callback page tests&lt;br /&gt;
&lt;br /&gt;
'''OidcLogin Component'''&lt;br /&gt;
* '''TODO''' Renders dropdown with providers when GET /auth/providers returns a non-empty list&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers returns an empty array&lt;br /&gt;
* '''TODO''' Renders nothing when GET /auth/providers fails&lt;br /&gt;
* '''TODO''' Calls POST /auth/client-select with selected provider id on dropdown change&lt;br /&gt;
* '''TODO''' Redirects browser to returned authorization URL on successful client-select&lt;br /&gt;
* '''TODO''' Does not redirect when client-select fails&lt;br /&gt;
* '''TODO''' Existing login form remains visible and functional alongside the dropdown&lt;br /&gt;
&lt;br /&gt;
'''OidcCallback Component'''&lt;br /&gt;
* '''TODO''' Posts code and state to POST /auth/callback on mount&lt;br /&gt;
* '''TODO''' Stores session JWT and dispatches auth state on success&lt;br /&gt;
* '''TODO''' Redirects to dashboard on successful login&lt;br /&gt;
* '''TODO''' Displays error alert and redirects to login on backend failure (e.g. no matching account)&lt;br /&gt;
* '''TODO''' Displays error and redirects to login when IdP returns an error parameter (e.g. user denied consent) without calling the backend&lt;br /&gt;
* '''TODO''' Redirects to login when code or state query parameters are missing&lt;br /&gt;
* '''TODO''' Shows &amp;quot;Completing login...&amp;quot; message while request is in flight&lt;br /&gt;
&lt;br /&gt;
=== Routes ===&lt;br /&gt;
 GET  /auth/providers      → oidc_login#providers&lt;br /&gt;
 POST /auth/client-select  → oidc_login#client_select&lt;br /&gt;
 POST /auth/callback       → oidc_login#callback&lt;br /&gt;
 GET  /auth/callback       → React OidcCallback component&lt;br /&gt;
&lt;br /&gt;
== Development Stories (Planning) ==&lt;br /&gt;
&lt;br /&gt;
=== Story 1: Backend — OIDC Provider Configuration ===&lt;br /&gt;
'''As a''' developer, '''I want''' provider configurations loaded from a YAML file at boot, '''so that''' new OIDC providers can be added without code changes.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create &amp;lt;code&amp;gt;config/oidc_providers.yml&amp;lt;/code&amp;gt; with ERB support for injecting secrets from environment variables.&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; class that loads and validates the YAML at boot, exposing methods to list providers and look up a provider by id.&lt;br /&gt;
* Define the config file path as a constant (&amp;lt;code&amp;gt;CONFIG_FILE&amp;lt;/code&amp;gt;) for clarity.&lt;br /&gt;
* Validate required keys: &amp;lt;code&amp;gt;display_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;issuer&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;client_secret&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;redirect_uri&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;scopes&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Raise a clear boot-time error if required values are missing via &amp;lt;code&amp;gt;config/initializers/oidc.rb&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Add unit tests for config loading, validation, and missing key detection.&lt;br /&gt;
&lt;br /&gt;
=== Story 2: Backend — Auth Requests Table ===&lt;br /&gt;
'''As a''' developer, '''I want''' a database-backed store for OIDC state, nonce, and PKCE values, '''so that''' the backend can validate callbacks without relying on cookies.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Generate an ActiveRecord migration for &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; with columns: &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; (string, indexed, unique), &amp;lt;code&amp;gt;nonce&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;code_verifier&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Create the &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; model with a uniqueness validation on state, an &amp;lt;code&amp;gt;expired&amp;lt;/code&amp;gt; scope, and a &amp;lt;code&amp;gt;cleanup_expired&amp;lt;/code&amp;gt; class method.&lt;br /&gt;
* Add a rake task or scheduled job to periodically clean up expired rows.&lt;br /&gt;
* Add unit tests for creation, lookup, expiry, and deletion.&lt;br /&gt;
&lt;br /&gt;
=== Story 3: Backend — Provider List Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; endpoint, '''so that''' the login page can dynamically render provider options.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create a controller action that returns a JSON array of &amp;lt;code&amp;gt;{ id, name }&amp;lt;/code&amp;gt; from &amp;lt;code&amp;gt;OidcConfig.public_list&amp;lt;/code&amp;gt;.&lt;br /&gt;
* No secrets or endpoint URLs are included in the response.&lt;br /&gt;
* Add a request spec covering the response format and a case with multiple providers.&lt;br /&gt;
&lt;br /&gt;
=== Story 4: Backend — Client Select Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; endpoint that returns an authorization URL, '''so that''' the frontend can redirect the user to the identity provider.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept a &amp;lt;code&amp;gt;provider&amp;lt;/code&amp;gt; param and look up the provider config.&lt;br /&gt;
* Generate cryptographically random state, nonce, and PKCE code verifier/challenge.&lt;br /&gt;
* Insert a row into &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Construct and return the authorization URL with client_id, redirect_uri, scopes, state, nonce, and code_challenge.&lt;br /&gt;
* Return a 404 if the provider is unknown.&lt;br /&gt;
* Add request specs covering the happy path, unknown provider, and that the auth_requests row is created.&lt;br /&gt;
&lt;br /&gt;
=== Story 5: Backend — Callback Endpoint ===&lt;br /&gt;
'''As a''' frontend developer, '''I want''' a &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt; endpoint that exchanges the authorization code for tokens and returns a session, '''so that''' the user is logged in after completing the OIDC flow.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Accept &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; params.&lt;br /&gt;
* Look up and delete the matching &amp;lt;code&amp;gt;auth_requests&amp;lt;/code&amp;gt; row. Reject if not found or expired.&lt;br /&gt;
* Exchange the code for tokens using the &amp;lt;code&amp;gt;openid_connect&amp;lt;/code&amp;gt; gem with the stored code_verifier.&lt;br /&gt;
* Verify the ID token signature (JWKS), issuer, client_id, and nonce.&lt;br /&gt;
* Extract the email claim and match to an existing local user.&lt;br /&gt;
* On match: issue a session JWT using the same payload structure as &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and return it with the user profile.&lt;br /&gt;
* On no match: return a 404 error indicating no local account was found.&lt;br /&gt;
* Handle &amp;lt;code&amp;gt;ActiveRecord::RecordNotFound&amp;lt;/code&amp;gt; (invalid/expired state) and &amp;lt;code&amp;gt;InvalidToken&amp;lt;/code&amp;gt; (verification failure) exceptions.&lt;br /&gt;
* Add request specs covering the happy path, expired state, invalid state, and no matching user.&lt;br /&gt;
&lt;br /&gt;
=== Story 6: Frontend — Provider Dropdown on Login Page ===&lt;br /&gt;
'''As a''' user, '''I want''' to see a provider dropdown on the login page, '''so that''' I can authenticate with my school credentials.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Create an &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component that calls &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt; on mount.&lt;br /&gt;
* Render a &amp;lt;code&amp;gt;Form.Select&amp;lt;/code&amp;gt; dropdown with a disabled &amp;quot;Sign in with...&amp;quot; default option.&lt;br /&gt;
* If the request fails or returns empty, render nothing (no error, no placeholder).&lt;br /&gt;
* Existing login form remains unchanged and fully functional.&lt;br /&gt;
* Add component tests for rendering with providers and graceful fallback.&lt;br /&gt;
&lt;br /&gt;
=== Story 7: Frontend — Initiate OIDC Flow ===&lt;br /&gt;
'''As a''' user, '''I want''' selecting a provider from the dropdown to start the login flow, '''so that''' I am redirected to my school's login page.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* On selection change, &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;/auth/client-select&amp;lt;/code&amp;gt; with the provider id.&lt;br /&gt;
* On success, redirect the browser to the returned authorization URL via &amp;lt;code&amp;gt;window.location.href&amp;lt;/code&amp;gt;.&lt;br /&gt;
* On failure, log the error to the console.&lt;br /&gt;
* Add component tests for the redirect and error handling.&lt;br /&gt;
&lt;br /&gt;
=== Story 8: Frontend — Callback Route and Login Completion ===&lt;br /&gt;
'''As a''' user, '''I want''' to be logged in automatically after authenticating with my school, '''so that''' I don't have to take any additional steps.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt; route in the React router pointing to the &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt; component.&lt;br /&gt;
* Extract &amp;lt;code&amp;gt;code&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;state&amp;lt;/code&amp;gt; from query parameters and &amp;lt;code&amp;gt;POST&amp;lt;/code&amp;gt; them to &amp;lt;code&amp;gt;/auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* If the query parameters contain an &amp;lt;code&amp;gt;error&amp;lt;/code&amp;gt; param (e.g. user denied consent), display the error via the alert slice and redirect to login without calling the backend.&lt;br /&gt;
* On success: call &amp;lt;code&amp;gt;setAuthToken&amp;lt;/code&amp;gt;, persist session to localStorage, dispatch &amp;lt;code&amp;gt;authenticationActions.setAuthentication&amp;lt;/code&amp;gt;, and redirect to the dashboard — mirroring the existing password login flow.&lt;br /&gt;
* On failure: display an error message via the alert slice and redirect to the login page.&lt;br /&gt;
* Show a &amp;quot;Completing login...&amp;quot; message while the token exchange is in progress.&lt;br /&gt;
* Add component tests for success, provider error, and backend error scenarios.&lt;br /&gt;
&lt;br /&gt;
=== Story 9: Backend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC backend, '''so that''' I have confidence the endpoints and models work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add request specs for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Mock the identity provider's discovery, token, and JWKS endpoints to avoid external calls in tests.&lt;br /&gt;
* Cover error scenarios: expired state, invalid state, unknown provider, no matching user, invalid ID token.&lt;br /&gt;
* Add model specs for &amp;lt;code&amp;gt;AuthRequest&amp;lt;/code&amp;gt; covering creation, uniqueness, expiry scope, and cleanup.&lt;br /&gt;
* Add unit tests for &amp;lt;code&amp;gt;OidcConfig&amp;lt;/code&amp;gt; covering loading, validation, and missing key detection.&lt;br /&gt;
* Verify the existing &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; is unaffected.&lt;br /&gt;
&lt;br /&gt;
=== Story 10: Frontend — Tests ===&lt;br /&gt;
'''As a''' developer, '''I want''' test coverage for the OIDC frontend components, '''so that''' I have confidence the login flow and callback work correctly.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt;: renders dropdown when providers are returned, renders nothing on empty or failed response, triggers &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt; on selection.&lt;br /&gt;
* Add component tests for &amp;lt;code&amp;gt;OidcCallback&amp;lt;/code&amp;gt;: posts code and state to backend on mount, redirects to dashboard on success, displays error and redirects to login on failure, handles IdP error parameter without calling the backend.&lt;br /&gt;
* Mock axios calls to avoid external requests in tests.&lt;br /&gt;
* Verify the existing login page renders and functions correctly with and without the &amp;lt;code&amp;gt;OidcLogin&amp;lt;/code&amp;gt; component.&lt;br /&gt;
&lt;br /&gt;
=== Additional Refactoring Stories ===&lt;br /&gt;
&lt;br /&gt;
==== Story 11: Backend — Unified Session Response ====&lt;br /&gt;
'''As a''' developer, '''I want''' the session object returned to the frontend clearly defined and reusable by all login flows, '''so that''' the frontend can rely on a consistent response shape regardless of authentication method.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Extract the JWT payload construction and token issuance logic from &amp;lt;code&amp;gt;AuthenticationController#login&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;OidcLoginController#callback&amp;lt;/code&amp;gt; into a shared method on the &amp;lt;code&amp;gt;User&amp;lt;/code&amp;gt; model (e.g. &amp;lt;code&amp;gt;user.generate_jwt&amp;lt;/code&amp;gt;).&lt;br /&gt;
* Define a consistent response structure (e.g. &amp;lt;code&amp;gt;{ token, user: { id, name, full_name, role, institution_id } }&amp;lt;/code&amp;gt;) and use it in both controllers.&lt;br /&gt;
* Update the existing password login endpoint to use the shared method without changing its external response shape.&lt;br /&gt;
* Add or update request specs for both login endpoints to assert the response structure matches.&lt;br /&gt;
&lt;br /&gt;
==== Story 12: Frontend — Externalize Hardcoded Configuration ====&lt;br /&gt;
'''As a''' developer, '''I want''' all hardcoded values moved to isolated configuration files, '''so that''' environment-specific settings can be changed without code modifications.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Move the frontend API base URL to an environment variable or shared config file (e.g. &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; with &amp;lt;code&amp;gt;REACT_APP_API_URL&amp;lt;/code&amp;gt;) and replace all hardcoded references.&lt;br /&gt;
* Ensure all existing tests continue to pass after the extraction.&lt;br /&gt;
&lt;br /&gt;
==== Story 13: Backend — Swagger Documentation for Provider Endpoints ====&lt;br /&gt;
'''As a''' developer, '''I want''' the OIDC provider endpoints documented in Swagger, '''so that''' frontend developers and future contributors can understand the API contract without reading the source code.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add Swagger/OpenAPI annotations for &amp;lt;code&amp;gt;GET /auth/providers&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;POST /auth/client-select&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;POST /auth/callback&amp;lt;/code&amp;gt;.&lt;br /&gt;
* Document request parameters, response schemas (including success and error shapes), and HTTP status codes for each endpoint.&lt;br /&gt;
* Include example request and response payloads.&lt;br /&gt;
* Verify the endpoints appear correctly in the generated Swagger UI.&lt;br /&gt;
&lt;br /&gt;
==== Story 14: Backend — Cleanup Expired Auth Requests ====&lt;br /&gt;
'''As a''' developer, '''I want''' expired auth request rows cleaned up automatically, '''so that''' the table does not grow unbounded from abandoned login attempts.&lt;br /&gt;
&lt;br /&gt;
'''Acceptance Criteria:'''&lt;br /&gt;
* Add a recurring ActiveJob solid_queue &amp;lt;code&amp;gt;auth_requests:cleanup&amp;lt;/code&amp;gt; that calls &amp;lt;code&amp;gt;AuthRequest.cleanup_expired&amp;lt;/code&amp;gt;.&lt;br /&gt;
* The task deletes all rows with &amp;lt;code&amp;gt;created_at&amp;lt;/code&amp;gt; older than 5 minutes.&lt;br /&gt;
* Schedule the task to run periodically (e.g. 24 hours).&lt;br /&gt;
* Add a test verifying that only expired rows are deleted.&lt;br /&gt;
&lt;br /&gt;
== Demo ==&lt;br /&gt;
&lt;br /&gt;
todo add screenshots of oidc login at each step&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026&amp;diff=167953</id>
		<title>CSC/ECE 517 Spring 2026</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026&amp;diff=167953"/>
		<updated>2026-04-14T21:36:05Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;* [[CSC/ECE 517 Spring 2026 - E2600. Reimplement review_mapping_controller]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2601. Reimplement student quizzes]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2602. Reimplement student task view]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2603. Implement ViewSubmissions frontend]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2606. Finishing Import and Export helper module]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2607. ResponseController Frontend]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2609. Review calibration]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2610. Teams hierarchy testing]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2615. Testing User model and Users controller]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2614. Role-based reviewing]]&lt;br /&gt;
* [[CSC/ECE 517 Spring 2026 - E2618. Support OIDC Logins]]&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167767</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167767"/>
		<updated>2026-04-07T01:37:18Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
When the user enters the website, they are presented with login fields. If they do not remember their password, they can select the ''Forgot password?'' link.&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordLogin.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;The user is now presented with a new window. This is where they will input their email to receive a token for resetting their password.&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
=== Results - Frontend ===&lt;br /&gt;
* Implemented two new authentication pages — '''Forgot Password''' and '''Reset Password''' — enabling users to recover account access via a tokenized email link&lt;br /&gt;
* Both pages follow the project's established conventions: React Bootstrap layout, Formik form state management, and Yup schema validation with inline field-level error messages&lt;br /&gt;
* The forgot password flow sends a &amp;quot;POST /password_resets&amp;quot; request with the user's email; the reset flow reads a &amp;quot;?token=&amp;quot; query parameter from the email link and sends a &amp;quot;PUT /password_resets/:token&amp;quot; request with the new password&lt;br /&gt;
* Added a null-token guard on the reset page that redirects to login if no token is present in the URL, covering cases like direct navigation or broken email links&lt;br /&gt;
* Error handling covers all failure scenarios: server-provided error messages are shown when available, with a generic fallback for network errors and unexpected response shapes&lt;br /&gt;
* The login page already contained a &amp;quot;Forgot password?&amp;quot; link pointing to &amp;quot;/forgot-password&amp;quot;; both new routes were already registered in &amp;quot;App.tsx&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Results - Backend ===&lt;br /&gt;
* '''app/controllers/passwords_controller.rb — Implements two endpoints: POST /password_resets to trigger a reset email (silently no-ops if email not found to prevent enumeration) and &amp;quot;PATCH /password_resets/:token&amp;quot; to validate the token and apply the new password. Both actions skip JWT authentication since the user is logged out.&lt;br /&gt;
* '''user.rb''' — Added &amp;quot;generates_token_for :password_reset, expires_in: 15.minutes&amp;quot; using the Rails 7.1 token API. The token is HMAC-signed, time-limited, and fingerprinted against &amp;quot;password_salt&amp;quot; so it automatically invalidates after the password is changed.&lt;br /&gt;
* '''user_mailer.rb''' — Added &amp;quot;send_password_reset_email&amp;quot; which constructs a reset URL pointing to the frontend (&amp;quot;FRONTEND_URL/password_edit/check_reset_url?token=&amp;lt;token&amp;gt;&amp;quot;) and delivers the email asynchronously via &amp;quot;deliver_later&amp;quot;.&lt;br /&gt;
* '''app/views/user_mailer/send_password_reset_email.html.erb''' — HTML email template rendered by the mailer, containing the reset link and a note that it expires in 15 minutes.&lt;br /&gt;
* '''routes.rb''' — Registered &amp;quot;resources :password_resets, only: [:create, :update], controller: &amp;quot;passwords&amp;quot;, param: :token&amp;quot; to wire up the two endpoints with the token as the URL parameter.&lt;br /&gt;
* '''en.yml''' — Added all user-facing strings under the &amp;quot;password_reset&amp;quot; namespace (email subject, success messages, and error messages) for i18n consistency.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/file/d/1AtfkTxzgnguZvrckcoby2sUxKAPimElM/view?usp=sharing Frontend And Backend Test Verification]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
=== Test Results ===&lt;br /&gt;
* 30 total tests across the backend (16) and frontend (14), all passing with zero failures&lt;br /&gt;
* Backend uses RSpec to cover all controller endpoints (&amp;quot;POST /password_resets&amp;quot;, &amp;quot;PUT /password_resets/:token&amp;quot;), the &amp;quot;UserMailer&amp;quot;, and &amp;quot;User&amp;quot; model email normalization — including edge cases like invalid tokens, expired tokens (15-minute TTL), short passwords, and email enumeration protection&lt;br /&gt;
* Frontend uses Vitest + React Testing Library to test both &amp;quot;ForgotPassword&amp;quot; and &amp;quot;ResetPassword&amp;quot; components, covering rendering, form validation (empty fields, invalid email, mismatched/short passwords), success flows, network errors, and server-returned error messages&lt;br /&gt;
* Feature-specific backend coverage is estimated at ~95–100% for &amp;quot;PasswordResetsController&amp;quot; and &amp;quot;UserMailer&amp;quot;; the reported 59.79% overall figure reflects measurement against the entire codebase&lt;br /&gt;
* Frontend coverage is 96.42%/98.5% across the two components, with the only gaps being defensive error-handling branches that are low-risk by nature&lt;br /&gt;
&lt;br /&gt;
=== Test Coverage ===&lt;br /&gt;
* Backend (reimplementation-back-end)&lt;br /&gt;
** spec/controllers/api/v1/password_resets_controller_spec.rb — new controller spec&lt;br /&gt;
** spec/mailers/user_mailer_spec.rb — new mailer spec&lt;br /&gt;
** spec/factories/users.rb — added :password_reset_user factory&lt;br /&gt;
* Frontend (reimplementation-front-end)&lt;br /&gt;
** src/pages/Authentication/tests/ForgotPassword.test.tsx — new test file&lt;br /&gt;
** src/pages/Authentication/tests/ResetPassword.test.tsx — new test file&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:ExpertizaPasswordLogin.png&amp;diff=167766</id>
		<title>File:ExpertizaPasswordLogin.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:ExpertizaPasswordLogin.png&amp;diff=167766"/>
		<updated>2026-04-07T01:30:30Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167765</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167765"/>
		<updated>2026-04-07T01:27:40Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Results - Backend */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
=== Results - Frontend ===&lt;br /&gt;
* Implemented two new authentication pages — '''Forgot Password''' and '''Reset Password''' — enabling users to recover account access via a tokenized email link&lt;br /&gt;
* Both pages follow the project's established conventions: React Bootstrap layout, Formik form state management, and Yup schema validation with inline field-level error messages&lt;br /&gt;
* The forgot password flow sends a &amp;quot;POST /password_resets&amp;quot; request with the user's email; the reset flow reads a &amp;quot;?token=&amp;quot; query parameter from the email link and sends a &amp;quot;PUT /password_resets/:token&amp;quot; request with the new password&lt;br /&gt;
* Added a null-token guard on the reset page that redirects to login if no token is present in the URL, covering cases like direct navigation or broken email links&lt;br /&gt;
* Error handling covers all failure scenarios: server-provided error messages are shown when available, with a generic fallback for network errors and unexpected response shapes&lt;br /&gt;
* The login page already contained a &amp;quot;Forgot password?&amp;quot; link pointing to &amp;quot;/forgot-password&amp;quot;; both new routes were already registered in &amp;quot;App.tsx&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Results - Backend ===&lt;br /&gt;
* '''app/controllers/passwords_controller.rb — Implements two endpoints: POST /password_resets to trigger a reset email (silently no-ops if email not found to prevent enumeration) and &amp;quot;PATCH /password_resets/:token&amp;quot; to validate the token and apply the new password. Both actions skip JWT authentication since the user is logged out.&lt;br /&gt;
* '''user.rb''' — Added &amp;quot;generates_token_for :password_reset, expires_in: 15.minutes&amp;quot; using the Rails 7.1 token API. The token is HMAC-signed, time-limited, and fingerprinted against &amp;quot;password_salt&amp;quot; so it automatically invalidates after the password is changed.&lt;br /&gt;
* '''user_mailer.rb''' — Added &amp;quot;send_password_reset_email&amp;quot; which constructs a reset URL pointing to the frontend (&amp;quot;FRONTEND_URL/password_edit/check_reset_url?token=&amp;lt;token&amp;gt;&amp;quot;) and delivers the email asynchronously via &amp;quot;deliver_later&amp;quot;.&lt;br /&gt;
* '''app/views/user_mailer/send_password_reset_email.html.erb''' — HTML email template rendered by the mailer, containing the reset link and a note that it expires in 15 minutes.&lt;br /&gt;
* '''routes.rb''' — Registered &amp;quot;resources :password_resets, only: [:create, :update], controller: &amp;quot;passwords&amp;quot;, param: :token&amp;quot; to wire up the two endpoints with the token as the URL parameter.&lt;br /&gt;
* '''en.yml''' — Added all user-facing strings under the &amp;quot;password_reset&amp;quot; namespace (email subject, success messages, and error messages) for i18n consistency.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/file/d/1AtfkTxzgnguZvrckcoby2sUxKAPimElM/view?usp=sharing Frontend And Backend Test Verification]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
=== Test Results ===&lt;br /&gt;
* 30 total tests across the backend (16) and frontend (14), all passing with zero failures&lt;br /&gt;
* Backend uses RSpec to cover all controller endpoints (&amp;quot;POST /password_resets&amp;quot;, &amp;quot;PUT /password_resets/:token&amp;quot;), the &amp;quot;UserMailer&amp;quot;, and &amp;quot;User&amp;quot; model email normalization — including edge cases like invalid tokens, expired tokens (15-minute TTL), short passwords, and email enumeration protection&lt;br /&gt;
* Frontend uses Vitest + React Testing Library to test both &amp;quot;ForgotPassword&amp;quot; and &amp;quot;ResetPassword&amp;quot; components, covering rendering, form validation (empty fields, invalid email, mismatched/short passwords), success flows, network errors, and server-returned error messages&lt;br /&gt;
* Feature-specific backend coverage is estimated at ~95–100% for &amp;quot;PasswordResetsController&amp;quot; and &amp;quot;UserMailer&amp;quot;; the reported 59.79% overall figure reflects measurement against the entire codebase&lt;br /&gt;
* Frontend coverage is 96.42%/98.5% across the two components, with the only gaps being defensive error-handling branches that are low-risk by nature&lt;br /&gt;
&lt;br /&gt;
=== Test Coverage ===&lt;br /&gt;
* Backend (reimplementation-back-end)&lt;br /&gt;
** spec/controllers/api/v1/password_resets_controller_spec.rb — new controller spec&lt;br /&gt;
** spec/mailers/user_mailer_spec.rb — new mailer spec&lt;br /&gt;
** spec/factories/users.rb — added :password_reset_user factory&lt;br /&gt;
* Frontend (reimplementation-front-end)&lt;br /&gt;
** src/pages/Authentication/tests/ForgotPassword.test.tsx — new test file&lt;br /&gt;
** src/pages/Authentication/tests/ResetPassword.test.tsx — new test file&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167764</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167764"/>
		<updated>2026-04-07T01:25:26Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Results - Backend */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
=== Results - Frontend ===&lt;br /&gt;
* Implemented two new authentication pages — '''Forgot Password''' and '''Reset Password''' — enabling users to recover account access via a tokenized email link&lt;br /&gt;
* Both pages follow the project's established conventions: React Bootstrap layout, Formik form state management, and Yup schema validation with inline field-level error messages&lt;br /&gt;
* The forgot password flow sends a &amp;quot;POST /password_resets&amp;quot; request with the user's email; the reset flow reads a &amp;quot;?token=&amp;quot; query parameter from the email link and sends a &amp;quot;PUT /password_resets/:token&amp;quot; request with the new password&lt;br /&gt;
* Added a null-token guard on the reset page that redirects to login if no token is present in the URL, covering cases like direct navigation or broken email links&lt;br /&gt;
* Error handling covers all failure scenarios: server-provided error messages are shown when available, with a generic fallback for network errors and unexpected response shapes&lt;br /&gt;
* The login page already contained a &amp;quot;Forgot password?&amp;quot; link pointing to &amp;quot;/forgot-password&amp;quot;; both new routes were already registered in &amp;quot;App.tsx&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Results - Backend ===&lt;br /&gt;
* '''&amp;quot;app/controllers/passwords_controller.rb&amp;quot; — Implements two endpoints: &amp;quot;POST /password_resets&amp;quot; to trigger a reset email (silently no-ops if email not found to prevent enumeration) and &amp;quot;PATCH /password_resets/:token&amp;quot; to validate the token and apply the new password. Both actions skip JWT authentication since the user is logged out.&lt;br /&gt;
* '''user.rb''' — Added &amp;quot;generates_token_for :password_reset, expires_in: 15.minutes&amp;quot; using the Rails 7.1 token API. The token is HMAC-signed, time-limited, and fingerprinted against &amp;quot;password_salt&amp;quot; so it automatically invalidates after the password is changed.&lt;br /&gt;
* '''user_mailer.rb''' — Added &amp;quot;send_password_reset_email&amp;quot; which constructs a reset URL pointing to the frontend (&amp;quot;FRONTEND_URL/password_edit/check_reset_url?token=&amp;lt;token&amp;gt;&amp;quot;) and delivers the email asynchronously via &amp;quot;deliver_later&amp;quot;.&lt;br /&gt;
* '''&amp;quot;app/views/user_mailer/send_password_reset_email.html.erb&amp;quot;''' — HTML email template rendered by the mailer, containing the reset link and a note that it expires in 15 minutes.&lt;br /&gt;
* '''routes.rb''' — Registered &amp;quot;resources :password_resets, only: [:create, :update], controller: &amp;quot;passwords&amp;quot;, param: :token&amp;quot; to wire up the two endpoints with the token as the URL parameter.&lt;br /&gt;
* '''en.yml''' — Added all user-facing strings under the &amp;quot;password_reset&amp;quot; namespace (email subject, success messages, and error messages) for i18n consistency.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/file/d/1AtfkTxzgnguZvrckcoby2sUxKAPimElM/view?usp=sharing Frontend And Backend Test Verification]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
=== Test Results ===&lt;br /&gt;
* 30 total tests across the backend (16) and frontend (14), all passing with zero failures&lt;br /&gt;
* Backend uses RSpec to cover all controller endpoints (&amp;quot;POST /password_resets&amp;quot;, &amp;quot;PUT /password_resets/:token&amp;quot;), the &amp;quot;UserMailer&amp;quot;, and &amp;quot;User&amp;quot; model email normalization — including edge cases like invalid tokens, expired tokens (15-minute TTL), short passwords, and email enumeration protection&lt;br /&gt;
* Frontend uses Vitest + React Testing Library to test both &amp;quot;ForgotPassword&amp;quot; and &amp;quot;ResetPassword&amp;quot; components, covering rendering, form validation (empty fields, invalid email, mismatched/short passwords), success flows, network errors, and server-returned error messages&lt;br /&gt;
* Feature-specific backend coverage is estimated at ~95–100% for &amp;quot;PasswordResetsController&amp;quot; and &amp;quot;UserMailer&amp;quot;; the reported 59.79% overall figure reflects measurement against the entire codebase&lt;br /&gt;
* Frontend coverage is 96.42%/98.5% across the two components, with the only gaps being defensive error-handling branches that are low-risk by nature&lt;br /&gt;
&lt;br /&gt;
=== Test Coverage ===&lt;br /&gt;
* Backend (reimplementation-back-end)&lt;br /&gt;
** spec/controllers/api/v1/password_resets_controller_spec.rb — new controller spec&lt;br /&gt;
** spec/mailers/user_mailer_spec.rb — new mailer spec&lt;br /&gt;
** spec/factories/users.rb — added :password_reset_user factory&lt;br /&gt;
* Frontend (reimplementation-front-end)&lt;br /&gt;
** src/pages/Authentication/tests/ForgotPassword.test.tsx — new test file&lt;br /&gt;
** src/pages/Authentication/tests/ResetPassword.test.tsx — new test file&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167763</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167763"/>
		<updated>2026-04-07T01:25:03Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Results - Backend */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
=== Results - Frontend ===&lt;br /&gt;
* Implemented two new authentication pages — '''Forgot Password''' and '''Reset Password''' — enabling users to recover account access via a tokenized email link&lt;br /&gt;
* Both pages follow the project's established conventions: React Bootstrap layout, Formik form state management, and Yup schema validation with inline field-level error messages&lt;br /&gt;
* The forgot password flow sends a &amp;quot;POST /password_resets&amp;quot; request with the user's email; the reset flow reads a &amp;quot;?token=&amp;quot; query parameter from the email link and sends a &amp;quot;PUT /password_resets/:token&amp;quot; request with the new password&lt;br /&gt;
* Added a null-token guard on the reset page that redirects to login if no token is present in the URL, covering cases like direct navigation or broken email links&lt;br /&gt;
* Error handling covers all failure scenarios: server-provided error messages are shown when available, with a generic fallback for network errors and unexpected response shapes&lt;br /&gt;
* The login page already contained a &amp;quot;Forgot password?&amp;quot; link pointing to &amp;quot;/forgot-password&amp;quot;; both new routes were already registered in &amp;quot;App.tsx&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Results - Backend ===&lt;br /&gt;
* '''&amp;quot;app/controllers/passwords_controller.rb&amp;quot; — Implements two endpoints: &amp;quot;POST /password_resets&amp;quot; to trigger a reset email (silently no-ops if email not found to prevent enumeration) and &amp;quot;PATCH /password_resets/:token&amp;quot; to validate the token and apply the new password. Both actions skip JWT authentication since the user is logged out.&lt;br /&gt;
* '''user.rb''' — Added &amp;quot;generates_token_for :password_reset, expires_in: 15.minutes&amp;quot; using the Rails 7.1 token API. The token is HMAC-signed, time-limited, and fingerprinted against &amp;quot;password_salt&amp;quot; so it automatically invalidates after the password is changed.&lt;br /&gt;
* '''user_mailer.rb''' — Added &amp;quot;send_password_reset_email&amp;quot; which constructs a reset URL pointing to the frontend (&amp;quot;FRONTEND_URL/password_edit/check_reset_url?token=&amp;lt;token&amp;gt;&amp;quot;) and delivers the email asynchronously via &amp;quot;deliver_later&amp;quot;.&lt;br /&gt;
* '''&amp;quot;app/views/user_mailer/send_password_reset_email.html.erb&amp;quot;''' — HTML email template rendered by the mailer, containing the reset link and a note that it expires in 15 minutes.&lt;br /&gt;
* '''routes.rb''' — Registered &amp;quot;resources :password_resets, only: [:create, :update], controller: &amp;quot;passwords&amp;quot;, param: :token&amp;quot; to wire up the two endpoints with the token as the URL parameter.&lt;br /&gt;
- '''en.yml''' — Added all user-facing strings under the &amp;quot;password_reset&amp;quot; namespace (email subject, success messages, and error messages) for i18n consistency.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/file/d/1AtfkTxzgnguZvrckcoby2sUxKAPimElM/view?usp=sharing Frontend And Backend Test Verification]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
=== Test Results ===&lt;br /&gt;
* 30 total tests across the backend (16) and frontend (14), all passing with zero failures&lt;br /&gt;
* Backend uses RSpec to cover all controller endpoints (&amp;quot;POST /password_resets&amp;quot;, &amp;quot;PUT /password_resets/:token&amp;quot;), the &amp;quot;UserMailer&amp;quot;, and &amp;quot;User&amp;quot; model email normalization — including edge cases like invalid tokens, expired tokens (15-minute TTL), short passwords, and email enumeration protection&lt;br /&gt;
* Frontend uses Vitest + React Testing Library to test both &amp;quot;ForgotPassword&amp;quot; and &amp;quot;ResetPassword&amp;quot; components, covering rendering, form validation (empty fields, invalid email, mismatched/short passwords), success flows, network errors, and server-returned error messages&lt;br /&gt;
* Feature-specific backend coverage is estimated at ~95–100% for &amp;quot;PasswordResetsController&amp;quot; and &amp;quot;UserMailer&amp;quot;; the reported 59.79% overall figure reflects measurement against the entire codebase&lt;br /&gt;
* Frontend coverage is 96.42%/98.5% across the two components, with the only gaps being defensive error-handling branches that are low-risk by nature&lt;br /&gt;
&lt;br /&gt;
=== Test Coverage ===&lt;br /&gt;
* Backend (reimplementation-back-end)&lt;br /&gt;
** spec/controllers/api/v1/password_resets_controller_spec.rb — new controller spec&lt;br /&gt;
** spec/mailers/user_mailer_spec.rb — new mailer spec&lt;br /&gt;
** spec/factories/users.rb — added :password_reset_user factory&lt;br /&gt;
* Frontend (reimplementation-front-end)&lt;br /&gt;
** src/pages/Authentication/tests/ForgotPassword.test.tsx — new test file&lt;br /&gt;
** src/pages/Authentication/tests/ResetPassword.test.tsx — new test file&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167762</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167762"/>
		<updated>2026-04-07T01:24:39Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Password Reset Workflow */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
=== Results - Frontend ===&lt;br /&gt;
* Implemented two new authentication pages — '''Forgot Password''' and '''Reset Password''' — enabling users to recover account access via a tokenized email link&lt;br /&gt;
* Both pages follow the project's established conventions: React Bootstrap layout, Formik form state management, and Yup schema validation with inline field-level error messages&lt;br /&gt;
* The forgot password flow sends a &amp;quot;POST /password_resets&amp;quot; request with the user's email; the reset flow reads a &amp;quot;?token=&amp;quot; query parameter from the email link and sends a &amp;quot;PUT /password_resets/:token&amp;quot; request with the new password&lt;br /&gt;
* Added a null-token guard on the reset page that redirects to login if no token is present in the URL, covering cases like direct navigation or broken email links&lt;br /&gt;
* Error handling covers all failure scenarios: server-provided error messages are shown when available, with a generic fallback for network errors and unexpected response shapes&lt;br /&gt;
* The login page already contained a &amp;quot;Forgot password?&amp;quot; link pointing to &amp;quot;/forgot-password&amp;quot;; both new routes were already registered in &amp;quot;App.tsx&amp;quot;&lt;br /&gt;
&lt;br /&gt;
=== Results - Backend ===&lt;br /&gt;
* '''&amp;quot;app/controllers/passwords_controller.rb&amp;quot;** — Implements two endpoints: &amp;quot;POST /password_resets&amp;quot; to trigger a reset email (silently no-ops if email not found to prevent enumeration) and &amp;quot;PATCH /password_resets/:token&amp;quot; to validate the token and apply the new password. Both actions skip JWT authentication since the user is logged out.&lt;br /&gt;
* '''user.rb''' — Added &amp;quot;generates_token_for :password_reset, expires_in: 15.minutes&amp;quot; using the Rails 7.1 token API. The token is HMAC-signed, time-limited, and fingerprinted against &amp;quot;password_salt&amp;quot; so it automatically invalidates after the password is changed.&lt;br /&gt;
* '''user_mailer.rb''' — Added &amp;quot;send_password_reset_email&amp;quot; which constructs a reset URL pointing to the frontend (&amp;quot;FRONTEND_URL/password_edit/check_reset_url?token=&amp;lt;token&amp;gt;&amp;quot;) and delivers the email asynchronously via &amp;quot;deliver_later&amp;quot;.&lt;br /&gt;
* '''&amp;quot;app/views/user_mailer/send_password_reset_email.html.erb&amp;quot;''' — HTML email template rendered by the mailer, containing the reset link and a note that it expires in 15 minutes.&lt;br /&gt;
* '''routes.rb''' — Registered &amp;quot;resources :password_resets, only: [:create, :update], controller: &amp;quot;passwords&amp;quot;, param: :token&amp;quot; to wire up the two endpoints with the token as the URL parameter.&lt;br /&gt;
- '''en.yml''' — Added all user-facing strings under the &amp;quot;password_reset&amp;quot; namespace (email subject, success messages, and error messages) for i18n consistency.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/file/d/1AtfkTxzgnguZvrckcoby2sUxKAPimElM/view?usp=sharing Frontend And Backend Test Verification]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
=== Test Results ===&lt;br /&gt;
* 30 total tests across the backend (16) and frontend (14), all passing with zero failures&lt;br /&gt;
* Backend uses RSpec to cover all controller endpoints (&amp;quot;POST /password_resets&amp;quot;, &amp;quot;PUT /password_resets/:token&amp;quot;), the &amp;quot;UserMailer&amp;quot;, and &amp;quot;User&amp;quot; model email normalization — including edge cases like invalid tokens, expired tokens (15-minute TTL), short passwords, and email enumeration protection&lt;br /&gt;
* Frontend uses Vitest + React Testing Library to test both &amp;quot;ForgotPassword&amp;quot; and &amp;quot;ResetPassword&amp;quot; components, covering rendering, form validation (empty fields, invalid email, mismatched/short passwords), success flows, network errors, and server-returned error messages&lt;br /&gt;
* Feature-specific backend coverage is estimated at ~95–100% for &amp;quot;PasswordResetsController&amp;quot; and &amp;quot;UserMailer&amp;quot;; the reported 59.79% overall figure reflects measurement against the entire codebase&lt;br /&gt;
* Frontend coverage is 96.42%/98.5% across the two components, with the only gaps being defensive error-handling branches that are low-risk by nature&lt;br /&gt;
&lt;br /&gt;
=== Test Coverage ===&lt;br /&gt;
* Backend (reimplementation-back-end)&lt;br /&gt;
** spec/controllers/api/v1/password_resets_controller_spec.rb — new controller spec&lt;br /&gt;
** spec/mailers/user_mailer_spec.rb — new mailer spec&lt;br /&gt;
** spec/factories/users.rb — added :password_reset_user factory&lt;br /&gt;
* Frontend (reimplementation-front-end)&lt;br /&gt;
** src/pages/Authentication/tests/ForgotPassword.test.tsx — new test file&lt;br /&gt;
** src/pages/Authentication/tests/ResetPassword.test.tsx — new test file&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167761</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167761"/>
		<updated>2026-04-07T01:15:32Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Testing and Verification */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/file/d/1AtfkTxzgnguZvrckcoby2sUxKAPimElM/view?usp=sharing Frontend And Backend Test Verification]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
=== Test Results ===&lt;br /&gt;
* 30 total tests across the backend (16) and frontend (14), all passing with zero failures&lt;br /&gt;
* Backend uses RSpec to cover all controller endpoints (&amp;quot;POST /password_resets&amp;quot;, &amp;quot;PUT /password_resets/:token&amp;quot;), the &amp;quot;UserMailer&amp;quot;, and &amp;quot;User&amp;quot; model email normalization — including edge cases like invalid tokens, expired tokens (15-minute TTL), short passwords, and email enumeration protection&lt;br /&gt;
* Frontend uses Vitest + React Testing Library to test both &amp;quot;ForgotPassword&amp;quot; and &amp;quot;ResetPassword&amp;quot; components, covering rendering, form validation (empty fields, invalid email, mismatched/short passwords), success flows, network errors, and server-returned error messages&lt;br /&gt;
* Feature-specific backend coverage is estimated at ~95–100% for &amp;quot;PasswordResetsController&amp;quot; and &amp;quot;UserMailer&amp;quot;; the reported 59.79% overall figure reflects measurement against the entire codebase&lt;br /&gt;
* Frontend coverage is 96.42%/98.5% across the two components, with the only gaps being defensive error-handling branches that are low-risk by nature&lt;br /&gt;
&lt;br /&gt;
=== Test Coverage ===&lt;br /&gt;
* Backend (reimplementation-back-end)&lt;br /&gt;
** spec/controllers/api/v1/password_resets_controller_spec.rb — new controller spec&lt;br /&gt;
** spec/mailers/user_mailer_spec.rb — new mailer spec&lt;br /&gt;
** spec/factories/users.rb — added :password_reset_user factory&lt;br /&gt;
* Frontend (reimplementation-front-end)&lt;br /&gt;
** src/pages/Authentication/tests/ForgotPassword.test.tsx — new test file&lt;br /&gt;
** src/pages/Authentication/tests/ResetPassword.test.tsx — new test file&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167735</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167735"/>
		<updated>2026-04-04T02:06:13Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Motivation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/file/d/1AtfkTxzgnguZvrckcoby2sUxKAPimElM/view?usp=sharing Frontend And Backend Test Verification]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167734</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167734"/>
		<updated>2026-04-04T02:05:32Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/file/d/1AtfkTxzgnguZvrckcoby2sUxKAPimElM/view?usp=sharing Frontend And Backend Test Verification]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167621</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167621"/>
		<updated>2026-03-30T22:40:55Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Testing and Verification */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/file/d/1AtfkTxzgnguZvrckcoby2sUxKAPimElM/view?usp=sharing Frontend And Backend Test Verification]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167620</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167620"/>
		<updated>2026-03-30T22:40:10Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Testing and Verification */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/file/d/1AtfkTxzgnguZvrckcoby2sUxKAPimElM/view?usp=sharing Front End And Back End Test Verification]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167573</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167573"/>
		<updated>2026-03-30T00:12:07Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Testing and Verification */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/ Test Verification Video] -&amp;gt; TODO: Will record video once all feedback is addressed&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167572</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167572"/>
		<updated>2026-03-30T00:11:47Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Testing and Verification */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com/file/d/1Piw2dpUQ5ldI9X1mdROg4crYI9QWxuiP/view?usp=drive_link Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
You can also [https://www.youtube.com/watch?v=gWbL0l0GJ3c Watch it on YouTube]&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/ Test Verification Video] -&amp;gt; Will record video once all feedback is addressed&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167558</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167558"/>
		<updated>2026-03-23T23:23:02Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://youtu.be/o3vQ4M7kFYo Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/ Test Verification Video] -&amp;gt; TODO&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167557</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167557"/>
		<updated>2026-03-22T22:54:49Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Demo Video */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://www.youtube.com/watch?v=o3vQ4M7kFYo Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/ Test Verification Video] -&amp;gt; TODO&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167555</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167555"/>
		<updated>2026-03-19T21:59:03Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Testing and Verification */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com Click to watch the demo video] -&amp;gt; todo&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/ Test Verification Video] -&amp;gt; TODO&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167554</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167554"/>
		<updated>2026-03-19T21:55:28Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Testing and Verification */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com Click to watch the demo video] -&amp;gt; todo&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
[[File:Forgotyourpassword.png]]&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/ Test Verification Video]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* Verifying reset request behavior&lt;br /&gt;
* Verifying token validation&lt;br /&gt;
* Verifying password updates&lt;br /&gt;
* Verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Frontend Tests ===&lt;br /&gt;
&lt;br /&gt;
The front end implementation achieves these goals by verifying that:&lt;br /&gt;
* Pages render the correct UI elements (headings, inputs, and submit buttons)&lt;br /&gt;
* Form validation prevents submission with invalid or mismatched input and displays appropriate error messages&lt;br /&gt;
* Successful API calls dispatch the correct success alerts to the user&lt;br /&gt;
* Failed API calls are handled gracefully, surfacing either a generic fallback or a server-provided error message&lt;br /&gt;
* Token validation on the reset page redirects unauthenticated users back to login&lt;br /&gt;
&lt;br /&gt;
=== Backend Tests ===&lt;br /&gt;
&lt;br /&gt;
The back end implementation achieves these goals by verifying that:&lt;br /&gt;
* Submitting a forgot-password request always returns HTTP 200 regardless of whether the email exists, preventing user enumeration&lt;br /&gt;
* A valid reset token triggers a password reset email sent to the correct user, containing the correct subject and a tokenized reset URL&lt;br /&gt;
* Successfully submitting a valid token with a new password updates the user's stored password_digest and returns HTTP 200&lt;br /&gt;
* Invalid, malformed, or expired tokens (&amp;gt;15 minutes) are rejected with HTTP 422 and an appropriate error message&lt;br /&gt;
* Password validation rules (minimum 6 characters) are enforced during the reset flow&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167551</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167551"/>
		<updated>2026-03-15T22:45:58Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Security Tests */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com Click to watch the demo video] -&amp;gt; todo&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/ Test Verification Video]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests TODO===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167550</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167550"/>
		<updated>2026-03-15T22:35:10Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Testing and Verification */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com Click to watch the demo video] -&amp;gt; todo&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
[https://drive.google.com/ Test Verification Video]&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167549</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167549"/>
		<updated>2026-03-15T22:27:26Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Demo Video */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com Click to watch the demo video] -&amp;gt; todo&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167548</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167548"/>
		<updated>2026-03-15T22:25:52Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Demo Video ==&lt;br /&gt;
[https://drive.google.com Click to watch the demo video]&lt;br /&gt;
&lt;br /&gt;
== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167547</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167547"/>
		<updated>2026-03-15T22:18:08Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Collaborators */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
* Mentor: Prerak Manish Bhandari&lt;br /&gt;
* Jose Vargas&lt;br /&gt;
* John Weisz&lt;br /&gt;
* Jared Monseur&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167546</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167546"/>
		<updated>2026-03-15T22:07:06Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Routing */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167545</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167545"/>
		<updated>2026-03-15T22:06:36Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Security Considerations */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
'''Potential risks include:'''&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
'''The system addresses these risks:'''&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167544</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167544"/>
		<updated>2026-03-15T22:05:14Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Security Considerations */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
* Account enumeration&lt;br /&gt;
* Token leakage via URL&lt;br /&gt;
&lt;br /&gt;
The system addresses these risks with :&lt;br /&gt;
* A request cannot reach the password update logic without first passing token validation. The update action is gated by find_user_by_token as a before_action.&lt;br /&gt;
* Tokens produced by Rails' generates_token_for are HMAC-signed using the application's secret_key_base. They are not short numeric codes or predictable sequences — they are cryptographically random and verifiable only by the server.&lt;br /&gt;
* When a password is successfully updated via has_secure_password, the password_salt changes. This causes the embedded fingerprint to no longer match, immediately invalidating the used token. Old links cannot be reused even within the 15-minute window.&lt;br /&gt;
* The create action always returns 200 OK with the same message (&amp;quot;If the email exists, a reset link has been sent.&amp;quot;) regardless of whether the submitted email matches a real account.&lt;br /&gt;
* The 15-minute expiry limits the window of exposure, and the token self-invalidates after use, so a leaked-but-used token is harmless. A leaked-but-unused token within 15 minutes remains a risk, but this is a standard trade-off for URL-based reset flows.&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167543</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167543"/>
		<updated>2026-03-15T21:46:23Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Security Considerations */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* Unauthorized password changes&lt;br /&gt;
* Brute-force token guessing&lt;br /&gt;
* Replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167542</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167542"/>
		<updated>2026-03-15T21:45:13Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Testing Strategy */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167541</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167541"/>
		<updated>2026-03-15T21:44:21Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Password Reset Workflow */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Generation ===&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
&lt;br /&gt;
=== How it works - Token Validation ===&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
&lt;br /&gt;
=== How it works - Secure Password Update ===&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167540</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167540"/>
		<updated>2026-03-15T21:41:06Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Updating the Password */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167539</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167539"/>
		<updated>2026-03-15T21:03:38Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Token Management */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167538</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167538"/>
		<updated>2026-03-15T21:02:59Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* How it works — Tokens */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167537</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167537"/>
		<updated>2026-03-15T21:02:45Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* How it works */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works — Tokens ===&lt;br /&gt;
&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167536</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167536"/>
		<updated>2026-03-15T21:00:46Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* How it works */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* Tokens are generated using Rails 7.1's built-in generates_token_for API.&lt;br /&gt;
** When a reset is requested, PasswordsController#create calls @user.generate_token_for(:password_reset), which produces a cryptographically signed token containing:&lt;br /&gt;
*** A purpose tag (password_reset) so the token cannot be reused for other token-based operations.&lt;br /&gt;
*** An expiry timestamp embedded in the token (15-minute window).&lt;br /&gt;
*** A fingerprint derived from the last 10 characters of the user's password_salt, falling back to updated_at&lt;br /&gt;
* Validation occurs in PasswordsController#find_user_by_token, run as a before_action on the update endpoint:&lt;br /&gt;
** Rails' find_by_token_for performs several checks atomically:&lt;br /&gt;
*** Signature verification — the token's HMAC signature must be valid (tampered tokens fail immediately).&lt;br /&gt;
*** Purpose check — the token must have been generated for :password_reset specifically.&lt;br /&gt;
*** Expiry check — the embedded timestamp must be within the 15-minute window.&lt;br /&gt;
*** Fingerprint check — the password_salt fragment in the token must match the user's current state.&lt;br /&gt;
** If any check fails, nil is returned and the controller renders a 422 Unprocessable Entity with a generic error message (&amp;quot;The token has expired or is invalid.&amp;quot;).&lt;br /&gt;
*** This single error message intentionally does not distinguish between expired vs. tampered tokens, avoiding information leakage.&lt;br /&gt;
* Once the token is validated, the update action applies the new password.&lt;br /&gt;
** Strong parameters filter the request, permitting only :password and :password_confirmation&lt;br /&gt;
*** The user has to be able to confirm the password by writing it again.&lt;br /&gt;
** At the storage layer:&lt;br /&gt;
*** has_secure_password delegates hashing to BCrypt via Rails' ActiveModel::SecurePassword. The raw password is never stored.&lt;br /&gt;
*** password_confirmation matching is enforced by has_secure_password — Rails raises a validation error if the two fields don't match.&lt;br /&gt;
*** Minimum length validation (length: { minimum: 6 }) ensures weak passwords are rejected before reaching the database.&lt;br /&gt;
*** Upon a successful save, password_salt changes, which immediately invalidates the reset token used.&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167535</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167535"/>
		<updated>2026-03-15T20:46:25Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* TODO: explain how reset tokens are generated&lt;br /&gt;
* TODO: explain how tokens are validated&lt;br /&gt;
* TODO: explain how passwords are updated securely&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167534</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167534"/>
		<updated>2026-03-15T20:43:45Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Navigating to the URL presents the user with the following password reset page.''&amp;lt;br&amp;gt;&lt;br /&gt;
[[File:ExpertizaPasswordReset.png]]&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* TODO: explain how reset tokens are generated&lt;br /&gt;
* TODO: explain how tokens are validated&lt;br /&gt;
* TODO: explain how passwords are updated securely&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
Example structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def generate_reset_token&lt;br /&gt;
  # TODO: create secure token&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def token_valid?&lt;br /&gt;
  # TODO: verify token existence&lt;br /&gt;
  # TODO: check expiration&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=File:ExpertizaPasswordReset.png&amp;diff=167533</id>
		<title>File:ExpertizaPasswordReset.png</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=File:ExpertizaPasswordReset.png&amp;diff=167533"/>
		<updated>2026-03-15T20:39:18Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167532</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167532"/>
		<updated>2026-03-15T20:32:28Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for create. It looks up the user-submitted email address and does nothing if no user is found.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''Assuming the email is valid, a password token is dispatched along with an HTML format email.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
generates_token_for :password_reset, expires_in: 15.minutes do&lt;br /&gt;
  password_salt&amp;amp;.last(10) || updated_at.to_s&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''A before_action for update. It validates and decodes the reset token from the URL.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''If the reset token is invalid or expired(find_by_token_for returns nil), the method renders a 422 Unprocessable Entity error''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* TODO: explain how reset tokens are generated&lt;br /&gt;
* TODO: explain how tokens are validated&lt;br /&gt;
* TODO: explain how passwords are updated securely&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
Example structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def generate_reset_token&lt;br /&gt;
  # TODO: create secure token&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def token_valid?&lt;br /&gt;
  # TODO: verify token existence&lt;br /&gt;
  # TODO: check expiration&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167518</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167518"/>
		<updated>2026-03-13T01:10:12Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''TODO'' -&amp;gt; reformat to follow reset steps&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''TODO''-&amp;gt; reformat to follow reset steps&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''TODO''-&amp;gt; reformat to follow reset steps&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* TODO: explain how reset tokens are generated&lt;br /&gt;
* TODO: explain how tokens are validated&lt;br /&gt;
* TODO: explain how passwords are updated securely&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
Example structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def generate_reset_token&lt;br /&gt;
  # TODO: create secure token&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def token_valid?&lt;br /&gt;
  # TODO: verify token existence&lt;br /&gt;
  # TODO: check expiration&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167517</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167517"/>
		<updated>2026-03-13T01:09:14Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''TODO''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_email&lt;br /&gt;
  @user = User.find_by(email: params[:email])&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''TODO''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def find_user_by_token&lt;br /&gt;
  @user = User.find_by_token_for(:password_reset, params[:token])&lt;br /&gt;
&lt;br /&gt;
  unless @user&lt;br /&gt;
    render json: { error: I18n.t('password_reset.errors.token_expired') }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''TODO''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def password_params&lt;br /&gt;
  params.require(:user).permit(:password, :password_confirmation)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* TODO: explain how reset tokens are generated&lt;br /&gt;
* TODO: explain how tokens are validated&lt;br /&gt;
* TODO: explain how passwords are updated securely&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
Example structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def generate_reset_token&lt;br /&gt;
  # TODO: create secure token&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def token_valid?&lt;br /&gt;
  # TODO: verify token existence&lt;br /&gt;
  # TODO: check expiration&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167516</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167516"/>
		<updated>2026-03-13T01:06:43Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits their email to request a password reset.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;br&amp;gt;''When a user submits a new password using the token from the email link.''&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* TODO: explain how reset tokens are generated&lt;br /&gt;
* TODO: explain how tokens are validated&lt;br /&gt;
* TODO: explain how passwords are updated securely&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
Example structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def generate_reset_token&lt;br /&gt;
  # TODO: create secure token&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def token_valid?&lt;br /&gt;
  # TODO: verify token existence&lt;br /&gt;
  # TODO: check expiration&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167515</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167515"/>
		<updated>2026-03-13T01:02:42Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
The PasswordsController handles two operations in the password reset flow: initiating a reset request and completing it with a new password. Both actions bypass the standard JWT authentication (skip_before_action :authenticate_request!), as they are designed for unauthenticated users.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* TODO: explain how reset tokens are generated&lt;br /&gt;
* TODO: explain how tokens are validated&lt;br /&gt;
* TODO: explain how passwords are updated securely&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
Example structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def generate_reset_token&lt;br /&gt;
  # TODO: create secure token&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def token_valid?&lt;br /&gt;
  # TODO: verify token existence&lt;br /&gt;
  # TODO: check expiration&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167514</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167514"/>
		<updated>2026-03-13T01:00:52Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Implementation */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
TODO: Describe where the password reset logic is implemented.&lt;br /&gt;
&lt;br /&gt;
Example structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
# POST /password_resets&lt;br /&gt;
def create&lt;br /&gt;
  if @user&lt;br /&gt;
    token = @user.generate_token_for(:password_reset)&lt;br /&gt;
    UserMailer.send_password_reset_email(@user, token).deliver_later&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  # Always return a 200 OK to prevent email enumeration attacks&lt;br /&gt;
  render json: { message: I18n.t('password_reset.email_sent') }, status: :ok&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
# PATCH/PUT /password_resets/:token&lt;br /&gt;
def update&lt;br /&gt;
  if @user.update(password_params)&lt;br /&gt;
    render json: { message: I18n.t('password_reset.updated') }, status: :ok&lt;br /&gt;
  else&lt;br /&gt;
    render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* TODO: explain how reset tokens are generated&lt;br /&gt;
* TODO: explain how tokens are validated&lt;br /&gt;
* TODO: explain how passwords are updated securely&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
Example structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def generate_reset_token&lt;br /&gt;
  # TODO: create secure token&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def token_valid?&lt;br /&gt;
  # TODO: verify token existence&lt;br /&gt;
  # TODO: check expiration&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167513</id>
		<title>CSC/ECE 517 Spring 2026 - E2604. Finish Password Resets</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2604._Finish_Password_Resets&amp;diff=167513"/>
		<updated>2026-03-13T00:54:08Z</updated>

		<summary type="html">&lt;p&gt;Jcmonseu: /* Purpose */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== Expertiza Background ==&lt;br /&gt;
&lt;br /&gt;
=== What is Expertiza? ===&lt;br /&gt;
'''Expertiza''' is an open-source educational web application built with Ruby on Rails and jointly maintained by students and faculty at North Carolina State University (NCSU). It is available publicly on GitHub.&lt;br /&gt;
&lt;br /&gt;
It supports:&lt;br /&gt;
&lt;br /&gt;
* Creating and configuring assignments (individual or team-based).&lt;br /&gt;
* Allowing students to submit work (files, links, etc.).&lt;br /&gt;
* Enabling peer review and teammate evaluation.&lt;br /&gt;
* Letting instructors define rubrics and grading criteria.&lt;br /&gt;
&lt;br /&gt;
Expertiza is designed to promote learning through iterative feedback. Students review each other’s work, reflect, improve, and resubmit. Instructors can monitor progress, manage deadlines, and assess performance.&lt;br /&gt;
&lt;br /&gt;
== Reimplementation Repository ==&lt;br /&gt;
=== Front End Overview ===&lt;br /&gt;
The '''reimplementation-front-end''' project replaces Expertiza's aging Rails monolith (server-rendered ERB templates, jQuery, session cookies, no client-side state) with a decoupled React + TypeScript SPA using JWT auth, Redux, Formik/Yup forms, React Bootstrap, and Vite — separating the front end entirely from the Rails API back end.&lt;br /&gt;
=== Back End Overview ===&lt;br /&gt;
The '''reimplementation-back-end''' project transforms Expertiza from a server-rendered Rails monolith into a pure JSON REST API using stateless JWT/RSA-256 authentication, a centralized role-based authorization concern, and adds several new features including a Duties system for team role assignment, a MentoredTeam type, a TeamsParticipant join model replacing TeamsUser, an Item/Strategy pattern replacing the Question STI hierarchy, a StudentTask service object, multi-view grade reporting endpoints, a self-registration AccountRequest workflow, full invitation lifecycle management, and auto-generated Swagger API documentation.&lt;br /&gt;
&lt;br /&gt;
== Project Background ==&lt;br /&gt;
&lt;br /&gt;
=== Motivation ===&lt;br /&gt;
Account recovery and password reset functionality is a critical part of any authentication system. The current site lacks a functional password reset or &amp;quot;forgot password&amp;quot; feature, representing a significant gap in both usability and security.&lt;br /&gt;
&lt;br /&gt;
This created several issues:&lt;br /&gt;
&lt;br /&gt;
* Without a working reset mechanism, there is no sanctioned path for users to change this default credential after account creation. This constitutes a vulnerability that persists indefinitely if left unaddressed.&lt;br /&gt;
* A user who forgets their credentials has no means of recovering access independently.&lt;br /&gt;
* A standards-compliant password reset mechanism relies on time-limited, single-use tokens delivered to a verified email address. This ensures that only the legitimate account owner can initiate a reset. Without such a mechanism, any ad-hoc reset approach would be either insecure or unverifiable (e.g., allowing resets without proving email ownership).&lt;br /&gt;
&lt;br /&gt;
This project introduces a more structured workflow for handling password reset and account recovery operations.&lt;br /&gt;
&lt;br /&gt;
The goal is to provide a consistent and secure process for users who need to reset or recover their credentials while maintaining proper validation and access control.&lt;br /&gt;
&lt;br /&gt;
Typical operations in this workflow include:&lt;br /&gt;
&lt;br /&gt;
* requesting a password reset&lt;br /&gt;
* generating a secure reset token&lt;br /&gt;
* validating reset requests&lt;br /&gt;
* updating the user's password&lt;br /&gt;
* preventing unauthorized reset attempts&lt;br /&gt;
&lt;br /&gt;
The system should ensure that these steps are handled in a secure, predictable, and maintainable way.&lt;br /&gt;
&lt;br /&gt;
== Objectives ==&lt;br /&gt;
&lt;br /&gt;
=== 1. Implement a unified password reset workflow ===&lt;br /&gt;
&lt;br /&gt;
The system should support a consistent flow for recovering user accounts.&lt;br /&gt;
&lt;br /&gt;
Typical steps include:&lt;br /&gt;
&lt;br /&gt;
* User requests password reset&lt;br /&gt;
* System generates a reset token&lt;br /&gt;
* User receives reset instructions via email&lt;br /&gt;
* User accesses password reset page via email link&lt;br /&gt;
* User submits new password&lt;br /&gt;
* System validates and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow should be easy to maintain and extend.&lt;br /&gt;
&lt;br /&gt;
=== 2. Improve security and validation ===&lt;br /&gt;
&lt;br /&gt;
Password reset functionality must enforce security best practices.&lt;br /&gt;
&lt;br /&gt;
This may include:&lt;br /&gt;
&lt;br /&gt;
* Ensure forgot password mechanism adheres to [https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html#url-tokens industry standards].&lt;br /&gt;
* Ensure reset links are generated correctly across development, staging, and production environments.&lt;br /&gt;
* Validate forgot and reset password mechanism functions through unit, functional, performance, and integration testing.&lt;br /&gt;
&lt;br /&gt;
The system should ensure that only authorized password reset requests are processed.&lt;br /&gt;
&lt;br /&gt;
=== 3. Improve frontend robustness and validation ===&lt;br /&gt;
The password reset workflow involves several frontend steps, including requesting a reset link and submitting a new password. The frontend should handle various edge cases gracefully.&lt;br /&gt;
&lt;br /&gt;
This project includes a review and refinement of the frontend behavior to ensure consistent handling of errors and validation responses.&lt;br /&gt;
&lt;br /&gt;
Key areas of focus include:&lt;br /&gt;
&lt;br /&gt;
* Handling invalid or expired password reset tokens&lt;br /&gt;
* Properly displaying backend validation errors&lt;br /&gt;
* Ensuring clear success and failure messages for users&lt;br /&gt;
* Confirming that form validation aligns with backend rules&lt;br /&gt;
&lt;br /&gt;
The goal is to ensure that the password reset workflow remains reliable and user-friendly.&lt;br /&gt;
&lt;br /&gt;
== Core Changes ==&lt;br /&gt;
&lt;br /&gt;
In this project:&lt;br /&gt;
&lt;br /&gt;
* The '''User model''' or authentication model was enhanced.&lt;br /&gt;
* A new '''password reset workflow''' was introduced.&lt;br /&gt;
* Controller logic was updated to handle reset requests.&lt;br /&gt;
* Routes were added to support password reset endpoints.&lt;br /&gt;
* Tests were added to verify authentication and recovery behavior.&lt;br /&gt;
&lt;br /&gt;
== Password Reset Workflow ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
The password reset workflow allows users who have forgotten their password to regain access to their account securely.&lt;br /&gt;
&lt;br /&gt;
A typical password reset flow may look like:&lt;br /&gt;
&lt;br /&gt;
* User selects &amp;quot;Forgot Password&amp;quot; in the login page.&lt;br /&gt;
* User provides email address associated with account.&lt;br /&gt;
* System generates a reset token.&lt;br /&gt;
* System sends reset email containing link with password token in it.&lt;br /&gt;
* User navigates to password reset portal and creates a new password.&lt;br /&gt;
* System validates the request and updates credentials&lt;br /&gt;
&lt;br /&gt;
This workflow must ensure both usability and security.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
TODO: Describe where the password reset logic is implemented.&lt;br /&gt;
&lt;br /&gt;
Example structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def request_password_reset&lt;br /&gt;
  # TODO: locate user account&lt;br /&gt;
  # TODO: generate secure reset token&lt;br /&gt;
  # TODO: send reset instructions&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def reset_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: update password&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* TODO: explain how reset tokens are generated&lt;br /&gt;
* TODO: explain how tokens are validated&lt;br /&gt;
* TODO: explain how passwords are updated securely&lt;br /&gt;
&lt;br /&gt;
== Token Management ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
Reset tokens allow the system to verify that the password reset request is legitimate.&lt;br /&gt;
&lt;br /&gt;
Tokens typically have the following properties:&lt;br /&gt;
&lt;br /&gt;
* unique per request&lt;br /&gt;
* difficult to guess&lt;br /&gt;
* time-limited&lt;br /&gt;
* associated with a specific user account&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
Example structure:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def generate_reset_token&lt;br /&gt;
  # TODO: create secure token&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def token_valid?&lt;br /&gt;
  # TODO: verify token existence&lt;br /&gt;
  # TODO: check expiration&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* When a reset request is initiated, the system generates a token.&lt;br /&gt;
* The token is associated with the requesting user.&lt;br /&gt;
* The token is included in the password reset link.&lt;br /&gt;
* When the user submits a new password, the token is verified.&lt;br /&gt;
&lt;br /&gt;
If validation fails, the reset attempt is rejected.&lt;br /&gt;
&lt;br /&gt;
== Updating the Password ==&lt;br /&gt;
&lt;br /&gt;
=== Purpose ===&lt;br /&gt;
After the reset token has been verified, the system allows the user to submit a new password.&lt;br /&gt;
&lt;br /&gt;
=== Implementation ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
def update_password&lt;br /&gt;
  # TODO: validate token&lt;br /&gt;
  # TODO: validate password rules&lt;br /&gt;
  # TODO: save updated credentials&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== How it works ===&lt;br /&gt;
&lt;br /&gt;
* The system confirms that the reset request is valid.&lt;br /&gt;
* Password requirements are checked.&lt;br /&gt;
* The new password is stored securely.&lt;br /&gt;
* The reset token is invalidated to prevent reuse.&lt;br /&gt;
&lt;br /&gt;
== Security Considerations ==&lt;br /&gt;
&lt;br /&gt;
Password reset systems must be designed carefully to prevent abuse.&lt;br /&gt;
&lt;br /&gt;
Potential risks include:&lt;br /&gt;
&lt;br /&gt;
* unauthorized password changes&lt;br /&gt;
* brute-force token guessing&lt;br /&gt;
* replay attacks using old reset links&lt;br /&gt;
&lt;br /&gt;
To mitigate these risks, the system may include:&lt;br /&gt;
&lt;br /&gt;
* TODO: token expiration&lt;br /&gt;
* TODO: rate limiting&lt;br /&gt;
* TODO: secure password hashing&lt;br /&gt;
* TODO: audit logging&lt;br /&gt;
&lt;br /&gt;
== Routing ==&lt;br /&gt;
&lt;br /&gt;
Example routing structure for password reset operations:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;rb&amp;quot;&amp;gt;&lt;br /&gt;
resources :password_resets do&lt;br /&gt;
  collection do&lt;br /&gt;
    post :request_reset&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  member do&lt;br /&gt;
    patch :reset_password&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
TODO: Modify routing according to the project's implementation.&lt;br /&gt;
&lt;br /&gt;
== Testing Strategy ==&lt;br /&gt;
&lt;br /&gt;
=== Goals of Testing ===&lt;br /&gt;
&lt;br /&gt;
The testing strategy verifies that password reset functionality works correctly and securely.&lt;br /&gt;
&lt;br /&gt;
Key goals include:&lt;br /&gt;
&lt;br /&gt;
* verifying reset request behavior&lt;br /&gt;
* verifying token validation&lt;br /&gt;
* verifying password updates&lt;br /&gt;
* verifying proper error handling&lt;br /&gt;
&lt;br /&gt;
=== Model Tests ===&lt;br /&gt;
&lt;br /&gt;
Model tests may verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: token generation&lt;br /&gt;
* TODO: token expiration behavior&lt;br /&gt;
* TODO: password validation rules&lt;br /&gt;
&lt;br /&gt;
=== Controller Tests ===&lt;br /&gt;
&lt;br /&gt;
Controller tests verify:&lt;br /&gt;
&lt;br /&gt;
* TODO: reset request creation&lt;br /&gt;
* TODO: reset token validation&lt;br /&gt;
* TODO: successful password update&lt;br /&gt;
* TODO: rejection of invalid requests&lt;br /&gt;
&lt;br /&gt;
=== Security Tests ===&lt;br /&gt;
&lt;br /&gt;
Security tests ensure that:&lt;br /&gt;
&lt;br /&gt;
* invalid tokens are rejected&lt;br /&gt;
* expired tokens cannot be used&lt;br /&gt;
* password updates require valid authentication&lt;br /&gt;
&lt;br /&gt;
== Collaborators ==&lt;br /&gt;
&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;br /&gt;
* TODO&lt;/div&gt;</summary>
		<author><name>Jcmonseu</name></author>
	</entry>
</feed>