diff --git a/lib/msal-custom-auth/package.json b/lib/msal-custom-auth/package.json index c9276578be..9693bf86c7 100644 --- a/lib/msal-custom-auth/package.json +++ b/lib/msal-custom-auth/package.json @@ -13,14 +13,14 @@ }, "description": "Microsoft Authentication Library for Native Authentication", "type": "module", - "module": "dist/index.js", + "module": "dist/index.mjs", "types": "dist/index.d.ts", "main": "lib/msal-custom-auth.cjs", "exports": { ".": { "import": { "types": "./dist/index.d.ts", - "default": "./dist/index.js" + "default": "./dist/index.mjs" }, "require": { "types": "./lib/types/index.d.ts", diff --git a/lib/msal-custom-auth/src/core/network_client/custom_auth_api/SignInApiClient.ts b/lib/msal-custom-auth/src/core/network_client/custom_auth_api/SignInApiClient.ts index 621568188c..382e557932 100644 --- a/lib/msal-custom-auth/src/core/network_client/custom_auth_api/SignInApiClient.ts +++ b/lib/msal-custom-auth/src/core/network_client/custom_auth_api/SignInApiClient.ts @@ -113,7 +113,7 @@ export class SignInApiClient extends BaseApiClient { correlationId: string, ): Promise { // The client_info parameter is required for MSAL to return the uid and utid in the response. - requestData.client_info = true; + requestData["client_info"] = true; const result = await this.request( CustomAuthApiEndpoint.SIGNIN_TOKEN, diff --git a/lib/msal-custom-auth/src/index.ts b/lib/msal-custom-auth/src/index.ts index 3e55b5dc73..3e6b5ae7be 100644 --- a/lib/msal-custom-auth/src/index.ts +++ b/lib/msal-custom-auth/src/index.ts @@ -92,3 +92,5 @@ export { UserAlreadySignedInError } from "./core/error/UserAlreadySignedInError. // Components from msal_browser export { LogLevel } from "@azure/msal-browser"; + +export { SignInState } from "./core/auth_flow/AuthFlowStateBase.js"; diff --git a/samples/msal-custom-auth-samples/angular-sample/.editorconfig b/samples/msal-custom-auth-samples/angular-sample/.editorconfig index f166060da1..fb2337aa05 100644 --- a/samples/msal-custom-auth-samples/angular-sample/.editorconfig +++ b/samples/msal-custom-auth-samples/angular-sample/.editorconfig @@ -1,17 +1,20 @@ -# Editor configuration, see https://editorconfig.org -root = true - +# Editor configuration, see http://editorconfig.org [*] -charset = utf-8 indent_style = space -indent_size = 2 -insert_final_newline = true +indent_size = 4 +end_of_line = lf +charset = utf-8 trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 120 -[*.ts] -quote_type = single -ij_typescript_use_double_quotes = false +[*.json] +indent_size = 2 [*.md] -max_line_length = off +max_line_length = 140 +trim_trailing_whitespace = false + +[*.html] +max_line_length = 200 trim_trailing_whitespace = false diff --git a/samples/msal-custom-auth-samples/angular-sample/README.md b/samples/msal-custom-auth-samples/angular-sample/README.md index 99ce1074ac..0fa9f2f765 100644 --- a/samples/msal-custom-auth-samples/angular-sample/README.md +++ b/samples/msal-custom-auth-samples/angular-sample/README.md @@ -1,59 +1,160 @@ -# AngularSample +# MSAL Custom Auth Angular Sample -This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.1. +This sample demonstrates how to implement custom authentication flows in an Angular application using the Microsoft Authentication Library (MSAL) for JavaScript with custom authentication. -## Development server +## Overview -To start a local development server, run: +This sample showcases a complete authentication flow with username/password sign-in and OTP (One-Time Password) challenge handling. It demonstrates how to: -```bash -ng serve +1. Implement a sign-in form with username and password +2. Handle OTP challenges when required by the authentication service +3. Manage authentication state using Angular services +4. Use Angular standalone components and reactive forms + +## Project Structure + +``` +angular-sample/ +├── src/ +│ ├── app/ +│ │ ├── components/ +│ │ │ ├── sign-in/ # Sign-in component with username/password form +│ │ │ │ ├── sign-in.component.ts +│ │ │ │ ├── sign-in.component.html +│ │ │ │ └── sign-in.component.scss +│ │ │ └── otp/ # OTP component for handling verification codes +│ │ │ ├── otp.component.ts +│ │ │ ├── otp.component.html +│ │ │ └── otp.component.scss +│ │ ├── services/ +│ │ │ └── auth.service.ts # Authentication service for MSAL integration +│ │ ├── models/ +│ │ │ └── auth.models.ts # TypeScript interfaces for auth-related data +│ │ ├── app.component.ts # Root component +│ │ ├── app.component.html +│ │ ├── app.component.scss +│ │ ├── app.config.ts # App configuration +│ │ └── app.routes.ts # Angular routing configuration +│ ├── index.html +│ ├── main.ts +│ └── styles.scss +├── angular.json # Angular CLI configuration +├── package.json # Project dependencies +└── tsconfig.json # TypeScript configuration ``` -Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. +## Prerequisites + +- Node.js and npm +- Angular CLI +- A Microsoft Entra ID tenant with custom authentication enabled +- Client ID and authority URL for your application -## Code scaffolding +## Setup -Angular CLI includes powerful code scaffolding tools. To generate a new component, run: +1. Clone the repository: ```bash -ng generate component component-name +git clone https://github.com/AzureAD/microsoft-authentication-library-for-js.git +cd microsoft-authentication-library-for-js/samples/msal-custom-auth-samples/angular-sample ``` -For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: +2. Install dependencies: ```bash -ng generate --help +npm install ``` -## Building +3. Configure the authentication settings: + +Open `src/app/services/auth.service.ts` and update the MSAL configuration with your application's details: + +```typescript +const msalConfig: CustomAuthConfiguration = { + auth: { + clientId: "your-client-id", // Replace with your client ID + authority: "https://your-tenant.ciamlogin.com", // Replace with your CIAM authority + redirectUri: window.location.origin, + }, + cache: { + cacheLocation: "localStorage", + }, + customAuth: { + // Add any custom auth configuration here + }, +}; +``` -To build the project run: +4. Start the development server: ```bash -ng build +npm start ``` -This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. +5. Navigate to `http://localhost:4200` in your browser. + +## Authentication Flow + +This sample implements the following authentication flow: + +1. User enters username and password in the sign-in form +2. The application sends the credentials to the authentication service +3. If the service requires additional verification, an OTP challenge is presented +4. User enters the verification code +5. Upon successful authentication, the user is redirected to the home page + +## Key Components -## Running unit tests +### AuthService -To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command: +The `AuthService` manages the authentication state and provides methods for: + +- Initializing the MSAL instance +- Handling sign-in with username and password +- Processing OTP verification +- Managing authentication state +- Handling sign-out + +### SignInComponent + +The `SignInComponent` provides a user interface for: + +- Collecting username and password +- Displaying validation errors +- Showing loading states during authentication +- Conditionally displaying the OTP component when required + +### OtpComponent + +The `OtpComponent` handles: + +- Collecting the verification code +- Validating the code format +- Submitting the code to complete authentication +- Displaying error messages + +## Development server + +To start a local development server, run: ```bash -ng test +ng serve ``` -## Running end-to-end tests +Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. -For end-to-end (e2e) testing, run: +## Building + +To build the project run: ```bash -ng e2e +ng build ``` -Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. +This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. ## Additional Resources -For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. +- [Microsoft Authentication Library (MSAL) for JavaScript](https://github.com/AzureAD/microsoft-authentication-library-for-js) +- [Angular Documentation](https://angular.dev) +- [Microsoft Entra ID Documentation](https://learn.microsoft.com/en-us/entra/identity/) diff --git a/samples/msal-custom-auth-samples/angular-sample/cors.js b/samples/msal-custom-auth-samples/angular-sample/cors.js new file mode 100644 index 0000000000..faa95d7586 --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/cors.js @@ -0,0 +1,73 @@ +const http = require("http"); +const https = require("https"); +const url = require("url"); +const proxyConfig = require("./proxy.config"); + +const extraHeaders = [ + "x-client-SKU", + "x-client-VER", + "x-client-OS", + "x-client-CPU", + "x-client-current-telemetry", + "x-client-last-telemetry", + "client-request-id", +]; +http.createServer((req, res) => { + const reqUrl = url.parse(req.url); + const domain = url.parse(proxyConfig.proxy).hostname; + + // Set CORS headers for all responses including OPTIONS + const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization, " + extraHeaders.join(", "), + "Access-Control-Allow-Credentials": "true", + "Access-Control-Max-Age": "86400", // 24 hours + }; + + // Handle preflight OPTIONS request + if (req.method === "OPTIONS") { + res.writeHead(204, corsHeaders); + res.end(); + return; + } + + if (reqUrl.pathname.startsWith(proxyConfig.localApiPath)) { + const targetUrl = proxyConfig.proxy + reqUrl.pathname?.replace(proxyConfig.localApiPath, "") + (reqUrl.search || ""); + + console.log("Incoming request -> " + req.url + " ===> " + reqUrl.pathname); + + const proxyReq = https.request( + targetUrl, + { + method: req.method, + headers: { + ...req.headers, + host: domain, + }, + }, + (proxyRes) => { + res.writeHead(proxyRes.statusCode, { + ...proxyRes.headers, + ...corsHeaders, + }); + + proxyRes.pipe(res); + } + ); + + proxyReq.on("error", (err) => { + console.error("Error with the proxy request:", err); + res.writeHead(500, { "Content-Type": "text/plain" }); + res.end("Proxy error."); + }); + + req.pipe(proxyReq); + } else { + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("Not Found"); + } +}).listen(proxyConfig.port, () => { + console.log("CORS proxy running on http://localhost:3001"); + console.log("Proxying from " + proxyConfig.localApiPath + " ===> " + proxyConfig.proxy); +}); diff --git a/samples/msal-custom-auth-samples/angular-sample/package.json b/samples/msal-custom-auth-samples/angular-sample/package.json index d594772596..7c29ebc87c 100644 --- a/samples/msal-custom-auth-samples/angular-sample/package.json +++ b/samples/msal-custom-auth-samples/angular-sample/package.json @@ -6,10 +6,12 @@ "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", - "test": "ng test" + "test": "ng test", + "cors": "node cors.js" }, "private": true, "dependencies": { + "@azure/msal-custom-auth": "^0.0.1", "@angular/common": "^19.2.0", "@angular/compiler": "^19.2.0", "@angular/core": "^19.2.0", diff --git a/samples/msal-custom-auth-samples/angular-sample/proxy.config.js b/samples/msal-custom-auth-samples/angular-sample/proxy.config.js new file mode 100644 index 0000000000..c47703be01 --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/proxy.config.js @@ -0,0 +1,12 @@ +/** + * Proxy configuration for local development + * entryPath: The path to the API on the react app ex. /api + * proxy: The URL to proxy the requests + */ +const config = { + localApiPath: "/api", + port: 3001, + proxy: "https://spasamples.ciamlogin.com/1eb974cd-0dc5-40a6-9f68-94b19f5535c5", +}; + +module.exports = config; diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/app.component.html b/samples/msal-custom-auth-samples/angular-sample/src/app/app.component.html index 36093e1879..ec51eb24e1 100644 --- a/samples/msal-custom-auth-samples/angular-sample/src/app/app.component.html +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/app.component.html @@ -1,336 +1,12 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title }}

-

Congratulations! Your app is running. 🎉

+ -
- - - - - - - - - + - + diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/app.component.ts b/samples/msal-custom-auth-samples/angular-sample/src/app/app.component.ts index cc061db03a..468c325a36 100644 --- a/samples/msal-custom-auth-samples/angular-sample/src/app/app.component.ts +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/app.component.ts @@ -1,12 +1,13 @@ -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; +import { Component } from "@angular/core"; +import { RouterOutlet, RouterLink, RouterLinkActive } from "@angular/router"; @Component({ - selector: 'app-root', - imports: [RouterOutlet], - templateUrl: './app.component.html', - styleUrl: './app.component.scss' + selector: "app-root", + standalone: true, + imports: [RouterOutlet, RouterLink, RouterLinkActive], + templateUrl: "./app.component.html", + styleUrl: "./app.component.scss", }) export class AppComponent { - title = 'angular-sample'; + title = "Custom Auth Sample in Angular"; } diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/app.routes.ts b/samples/msal-custom-auth-samples/angular-sample/src/app/app.routes.ts index dc39edb5f2..beb4742bef 100644 --- a/samples/msal-custom-auth-samples/angular-sample/src/app/app.routes.ts +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/app.routes.ts @@ -1,3 +1,11 @@ -import { Routes } from '@angular/router'; +import { Routes } from "@angular/router"; +import { SignInComponent } from "./components/sign-in/sign-in.component"; +import { ProfileComponent } from "./components/profile/profile.component"; +import { OtpComponent } from "./components/otp/otp.component"; -export const routes: Routes = []; +export const routes: Routes = [ + { path: "", redirectTo: "sign-in", pathMatch: "full" }, + { path: "sign-in", component: SignInComponent }, + { path: "profile", component: ProfileComponent }, + { path: "otp", component: OtpComponent }, +]; diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/components/otp/otp.component.html b/samples/msal-custom-auth-samples/angular-sample/src/app/components/otp/otp.component.html new file mode 100644 index 0000000000..84b4cff7d3 --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/components/otp/otp.component.html @@ -0,0 +1,46 @@ +
+

Verification Code Required

+

Please enter the verification code that was sent to your email or phone.

+ +
+
+ + + @if (otpForm.get('code')?.invalid && otpForm.get('code')?.touched) { +
+ @if (otpForm.get('code')?.errors?.['required']) { + Verification code is required + } @if (otpForm.get('code')?.errors?.['minlength'] || otpForm.get('code')?.errors?.['maxlength']) { + Code must be {{ codeLength }} digits + } @if (otpForm.get('code')?.errors?.['pattern']) { + Code must contain only numbers + } +
+ } +
+ + @if (errorMessage) { +
+ {{ errorMessage }} +
+ } + + +
+
diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/components/otp/otp.component.scss b/samples/msal-custom-auth-samples/angular-sample/src/app/components/otp/otp.component.scss new file mode 100644 index 0000000000..7e59de947f --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/components/otp/otp.component.scss @@ -0,0 +1,84 @@ +.otp-container { + padding: 20px 0; + + h3 { + margin-top: 0; + margin-bottom: 16px; + color: #333; + font-size: 20px; + } +} + +.otp-description { + margin-bottom: 24px; + color: #666; + font-size: 16px; + line-height: 1.5; +} + +.form-group { + margin-bottom: 20px; + + label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: #555; + } + + input { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 16px; + letter-spacing: 4px; + text-align: center; + font-weight: 600; + transition: border-color 0.3s; + + &:focus { + outline: none; + border-color: #0078d4; + box-shadow: 0 0 0 2px rgba(0, 120, 212, 0.2); + } + + &.invalid { + border-color: #d83b01; + } + + &::placeholder { + color: #aaa; + letter-spacing: normal; + font-weight: normal; + } + } +} + +.error-message { + color: #d83b01; + font-size: 14px; + margin-top: 6px; +} + +.submit-button { + width: 100%; + padding: 12px; + background-color: #0078d4; + color: white; + border: none; + border-radius: 4px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.3s; + + &:hover { + background-color: #106ebe; + } + + &:disabled { + background-color: #ccc; + cursor: not-allowed; + } +} diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/components/otp/otp.component.ts b/samples/msal-custom-auth-samples/angular-sample/src/app/components/otp/otp.component.ts new file mode 100644 index 0000000000..c30bace345 --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/components/otp/otp.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { AuthService } from "../../services/auth.service"; + +@Component({ + selector: "app-otp", + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + templateUrl: "./otp.component.html", + styleUrls: ["./otp.component.scss"], +}) +export class OtpComponent implements OnInit { + otpForm!: FormGroup; + isLoading = false; + errorMessage = ""; + codeLength = 6; // Default value + + constructor(private formBuilder: FormBuilder, private authService: AuthService, private router: Router) {} + + ngOnInit(): void { + this.codeLength = this.authService.getCodeLength(); + this.initForm(); + } + + private initForm(): void { + this.otpForm = this.formBuilder.group({ + code: [ + "", + [ + Validators.required, + Validators.minLength(this.codeLength), + Validators.maxLength(this.codeLength), + Validators.pattern("^[0-9]*$"), + ], + ], + }); + } + + onSubmit(): void { + if (this.otpForm.invalid) { + return; + } + + this.isLoading = true; + this.errorMessage = ""; + + const { code } = this.otpForm.value; + + this.authService + .submitOtp(code) + .then(() => { + this.isLoading = false; + this.router.navigate(["/profile"]); + }) + .catch((error) => {}); + } +} diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/components/profile/profile.component.ts b/samples/msal-custom-auth-samples/angular-sample/src/app/components/profile/profile.component.ts new file mode 100644 index 0000000000..25a575c4d4 --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/components/profile/profile.component.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { CommonModule } from "@angular/common"; +import { AuthService } from "../../services/auth.service"; + +@Component({ + selector: "app-profile", + standalone: true, + imports: [CommonModule], + template: ` +
+

Welcome!

+
+
Username: {{ userData.username }}
+
{{ userData | json }} 
+
+ +
+ +
Loading user data...
+ `, + styles: [ + ` + .profile-container { + max-width: 600px; + margin: 2rem auto; + padding: 2rem; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .profile-info { + margin: 1.5rem 0; + overflow: auto; + } + + .info-item { + margin: 0.5rem 0; + padding: 0.5rem; + background-color: #f8f9fa; + border-radius: 4px; + } + + .sign-out-btn { + background-color: #dc3545; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + } + + .sign-out-btn:hover { + background-color: #c82333; + } + + .loading { + text-align: center; + padding: 2rem; + } + `, + ], +}) +export class ProfileComponent implements OnInit { + userData: any = null; + + constructor(private authService: AuthService, private router: Router) {} + + ngOnInit() { + this.userData = this.authService.getUserData(); + } + + signOut() { + this.authService.signOut(); + this.router.navigate(["/sign-in"]); + } +} diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/components/sign-in/sign-in.component.html b/samples/msal-custom-auth-samples/angular-sample/src/app/components/sign-in/sign-in.component.html new file mode 100644 index 0000000000..87b255449a --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/components/sign-in/sign-in.component.html @@ -0,0 +1,70 @@ + diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/components/sign-in/sign-in.component.scss b/samples/msal-custom-auth-samples/angular-sample/src/app/components/sign-in/sign-in.component.scss new file mode 100644 index 0000000000..39af82c96c --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/components/sign-in/sign-in.component.scss @@ -0,0 +1,102 @@ +.sign-in-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + background-color: #f5f5f5; + padding: 20px; +} + +.sign-in-card { + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + padding: 32px; + width: 100%; + max-width: 400px; + + h2 { + margin-top: 0; + margin-bottom: 24px; + color: #333; + font-size: 24px; + text-align: center; + } +} + +.form-group { + margin-bottom: 20px; + + label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: #555; + } + + input { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 16px; + transition: border-color 0.3s; + + &:focus { + outline: none; + border-color: #0078d4; + box-shadow: 0 0 0 2px rgba(0, 120, 212, 0.2); + } + + &.invalid { + border-color: #d83b01; + } + + &::placeholder { + color: #aaa; + } + } +} + +.error-message { + color: #d83b01; + font-size: 14px; + margin-top: 6px; +} + +.sign-in-button { + width: 100%; + padding: 12px; + background-color: #0078d4; + color: white; + border: none; + border-radius: 4px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.3s; + + &:hover { + background-color: #106ebe; + } + + &:disabled { + background-color: #ccc; + cursor: not-allowed; + } +} + +.otp-message { + margin-top: 20px; + padding: 16px; + background-color: #f0f6ff; + border-left: 4px solid #0078d4; + border-radius: 4px; + + p { + margin: 0; + color: #333; + font-size: 16px; + line-height: 1.5; + } +} diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/components/sign-in/sign-in.component.ts b/samples/msal-custom-auth-samples/angular-sample/src/app/components/sign-in/sign-in.component.ts new file mode 100644 index 0000000000..28ddf025eb --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/components/sign-in/sign-in.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { AuthService } from "../../services/auth.service"; +import { SignInState } from "@azure/msal-custom-auth"; + +@Component({ + selector: "app-sign-in", + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + templateUrl: "./sign-in.component.html", + styleUrls: ["./sign-in.component.scss"], +}) +export class SignInComponent implements OnInit { + signInForm!: FormGroup; + isLoading = false; + errorMessage = ""; + showOtpForm = false; + + constructor(private formBuilder: FormBuilder, private authService: AuthService, private router: Router) {} + + ngOnInit(): void { + this.signInForm = this.formBuilder.group({ + username: ["", [Validators.required, Validators.email]], + password: ["", Validators.required], + }); + } + + onSubmit(): void { + if (this.signInForm.invalid) { + return; + } + + this.isLoading = true; + this.errorMessage = ""; + + const { username, password } = this.signInForm.value; + + this.authService + .signIn(username, password) + .then((result) => { + this.isLoading = false; + if (result.state?.type === SignInState.Completed) { + this.router.navigate(["/profile"]); // Updated to navigate to profile + } else if (result.state?.type === SignInState.CodeRequired) { + this.router.navigate(["/otp"]); + } else { + this.errorMessage = "An error occurred during sign-in"; + } + }) + .catch((error) => { + this.isLoading = false; + this.errorMessage = error.message || "An error occurred during sign-in"; + }); + } +} diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/config/auth-config.ts b/samples/msal-custom-auth-samples/angular-sample/src/app/config/auth-config.ts new file mode 100644 index 0000000000..1841396bd2 --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/config/auth-config.ts @@ -0,0 +1,42 @@ +import { CustomAuthConfiguration, LogLevel } from "@azure/msal-custom-auth"; + +export const customAuthConfig: CustomAuthConfiguration = { + customAuth: { + challengeTypes: ["password", "oob", "redirect"], + authApiProxyUrl: "http://localhost:3001/api", + }, + auth: { + clientId: "", + authority: "", + redirectUri: "/", + postLogoutRedirectUri: "/", + navigateToLoginRequestUrl: false, + }, + cache: { + cacheLocation: "sessionStorage", + storeAuthStateInCookie: false, + }, + system: { + loggerOptions: { + loggerCallback: (level: LogLevel, message: string, containsPii: boolean) => { + if (containsPii) { + return; + } + switch (level) { + case LogLevel.Error: + console.error(message); + return; + case LogLevel.Info: + console.info(message); + return; + case LogLevel.Verbose: + console.debug(message); + return; + case LogLevel.Warning: + console.warn(message); + return; + } + }, + }, + }, +}; diff --git a/samples/msal-custom-auth-samples/angular-sample/src/app/services/auth.service.ts b/samples/msal-custom-auth-samples/angular-sample/src/app/services/auth.service.ts new file mode 100644 index 0000000000..53532d4fac --- /dev/null +++ b/samples/msal-custom-auth-samples/angular-sample/src/app/services/auth.service.ts @@ -0,0 +1,91 @@ +import { Injectable } from "@angular/core"; +import { + CustomAuthPublicClientApplication, + ICustomAuthPublicClientApplication, + SignInInputs, + SignInResult, + SignInState, +} from "@azure/msal-custom-auth"; +import { customAuthConfig } from "../config/auth-config"; + +@Injectable({ + providedIn: "root", +}) +export class AuthService { + private customAuthApp: ICustomAuthPublicClientApplication | null = null; + + private currentState: any = null; + private userData: any = null; + constructor() { + this.initializeMsal(); + } + + private async initializeMsal(): Promise { + try { + this.customAuthApp = await CustomAuthPublicClientApplication.create(customAuthConfig); + const currentAccount = this.customAuthApp.getCurrentAccount(); + if (currentAccount) { + this.userData = currentAccount.data?.getAccount(); + } + } catch (error) { + console.error("Error initializing MSAL:", error); + } + } + + public async signIn(username: string, password: string): Promise { + if (!this.customAuthApp) { + return Promise.reject(new Error("MSAL instance not initialized")); + } + const signInRequest: SignInInputs = { + username, + password, + scopes: ["openid", "profile", "email"], + }; + + return this.customAuthApp.signIn(signInRequest).then((result: SignInResult) => { + this.currentState = result.state; + if (result.state?.type === SignInState.Completed) { + this.userData = result.data?.getAccount(); + } + return result; + }); + } + + public submitOtp(code: string): Promise { + if (!this.customAuthApp) { + return Promise.reject(new Error("MSAL instance not initialized")); + } + + return this.currentState.submitCode(code).then((result: SignInResult) => { + if (result.state?.type === SignInState.Completed) { + this.userData = result.data; + } + + this.currentState = result.data; + return result; + }); + } + + public signOut(): void { + if (this.customAuthApp) { + this.userData = null; + //this.customAuthApp.signOut(); + sessionStorage.clear(); + } + } + + public isAuthenticated(): boolean { + return this.userData !== null; + } + + public getUserData(): any { + return this.userData; + } + + public getCodeLength(): number { + if (this.currentState && "codeLength" in this.currentState) { + return this.currentState.codeLength || 6; + } + return 6; + } +} diff --git a/samples/msal-custom-auth-samples/angular-sample/src/index.html b/samples/msal-custom-auth-samples/angular-sample/src/index.html index d246809247..76f6adba99 100644 --- a/samples/msal-custom-auth-samples/angular-sample/src/index.html +++ b/samples/msal-custom-auth-samples/angular-sample/src/index.html @@ -1,13 +1,13 @@ - + - - - AngularSample - - - - - - - + + + AngularSample + + + + + + + diff --git a/samples/msal-custom-auth-samples/angular-sample/src/main.ts b/samples/msal-custom-auth-samples/angular-sample/src/main.ts index 35b00f3463..9318a4455e 100644 --- a/samples/msal-custom-auth-samples/angular-sample/src/main.ts +++ b/samples/msal-custom-auth-samples/angular-sample/src/main.ts @@ -1,6 +1,5 @@ -import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { AppComponent } from './app/app.component'; +import { bootstrapApplication } from "@angular/platform-browser"; +import { appConfig } from "./app/app.config"; +import { AppComponent } from "./app/app.component"; -bootstrapApplication(AppComponent, appConfig) - .catch((err) => console.error(err)); +bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); diff --git a/samples/msal-custom-auth-samples/angular-sample/src/styles.scss b/samples/msal-custom-auth-samples/angular-sample/src/styles.scss index 90d4ee0072..abc720bccd 100644 --- a/samples/msal-custom-auth-samples/angular-sample/src/styles.scss +++ b/samples/msal-custom-auth-samples/angular-sample/src/styles.scss @@ -1 +1,147 @@ -/* You can add global styles to this file, and also import other style files */ +/* Global styles for the Angular sample */ + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background-color: #f9fafb; + color: #111827; + min-height: 100vh; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +.auth-container { + max-width: 400px; + margin: 2rem auto; + padding: 2rem; + background-color: white; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.auth-container h2 { + margin-bottom: 1.5rem; + color: #111827; + font-size: 1.5rem; + font-weight: 600; + text-align: center; +} + +/* Navigation styles */ +.nav-container { + background-color: white; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 1rem 2rem; +} + +.nav-content { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1200px; + margin: 0 auto; +} + +.nav-logo { + font-weight: 600; + font-size: 1.25rem; + color: #0078d4; +} + +.nav-links { + display: flex; + gap: 1.5rem; +} + +.nav-link { + color: #4b5563; + text-decoration: none; + font-weight: 500; + transition: color 0.2s ease; +} + +.nav-link:hover { + color: #0078d4; +} + +.nav-link.active { + color: #0078d4; + font-weight: 600; +} + +/* Form styles */ +.form-group { + margin-bottom: 1.25rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: #374151; +} + +.form-group input { + width: 100%; + padding: 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + font-size: 1rem; + transition: border-color 0.2s ease; +} + +.form-group input:focus { + outline: none; + border-color: #0078d4; + box-shadow: 0 0 0 2px rgba(0, 120, 212, 0.2); +} + +.form-group input.invalid { + border-color: #ef4444; +} + +.error-message { + color: #ef4444; + font-size: 0.875rem; + margin-top: 0.375rem; +} + +button { + width: 100%; + padding: 0.75rem; + background-color: #0078d4; + color: white; + border: none; + border-radius: 0.375rem; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease; +} + +button:hover { + background-color: #006cbe; +} + +button:disabled { + background-color: #9ca3af; + cursor: not-allowed; +} + +/* OTP styles */ +.otp-message { + margin-top: 1.5rem; + padding: 1rem; + background-color: #f0f9ff; + border-left: 4px solid #0078d4; + border-radius: 0.375rem; +} + +.otp-message p { + color: #374151; + font-size: 0.875rem; + line-height: 1.5; +}