Skip to content

Signin #7620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: custom-auth/main
Choose a base branch
from
Draft

Signin #7620

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import React, { createContext, useState, useEffect, useContext } from "react";
import {
ICustomAuthPublicClientApplication,
CustomAuthPublicClientApplication,
AuthFlowStateBase,
} from "@azure/msal-custom-auth";

interface AuthContextType {
app: ICustomAuthPublicClientApplication | undefined;
setApp: React.Dispatch<
React.SetStateAction<ICustomAuthPublicClientApplication | undefined>
>;
authState: AuthFlowStateBase | undefined; // Replace 'any' with your stateData type
setAuthState: React.Dispatch<
React.SetStateAction<AuthFlowStateBase | undefined>
>;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [app, setApp] = useState<
ICustomAuthPublicClientApplication | undefined
>(undefined);
const [authState, setAuthState] = useState<AuthFlowStateBase | undefined>(
undefined
);

useEffect(() => {
const initializeApp = async () => {
const app = await CustomAuthPublicClientApplication.create({
customAuth: {
challengeTypes: ["password", "oob", "redirect"],
authApiProxyUrl:
"https://myspafunctiont1.azurewebsites.net/api/ReverseProxy/",
},
auth: {
clientId: "d5e97fb9-24bb-418d-8e7a-4e1918303c92",
authority: "https://spasamples.ciamlogin.com/",
redirectUri: "/",
},
cache: {
cacheLocation: "sessionStorage",
storeAuthStateInCookie: false,
},
});
setApp(app);
};
initializeApp();
}, [setApp]);

return (
<AuthContext.Provider value={{ app, setApp, authState, setAuthState }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,36 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Navbar from "@/components/Navbar";
import "./globals.css";
import { AuthProvider } from "./context/AuthContext";

const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
variable: "--font-geist-sans",
subsets: ["latin"],
});

const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
variable: "--font-geist-mono",
subsets: ["latin"],
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
children,
}: Readonly<{
children: React.ReactNode;
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<Navbar />
{children}
</body>
</html>
);
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<AuthProvider>
<Navbar />
{children}
</AuthProvider>
</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,56 @@
'use client';

export default function SignIn() {
return (
<div className="auth-container">
<h2>Sign In</h2>
{/* Sign In form will be added here */}
</div>
);
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "../context/AuthContext";
import { SignInState } from "@azure/msal-custom-auth";

export default function SignInPage() {
const { app, setAuthState } = useAuth();
const [error, setError] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const router = useRouter();

const handleSignIn = async () => {
if (!app) return;

const result = await app.signIn({
username: "[email protected]",
});
setAuthState(result.state); // Update the auth state in the context

if (!result.state) {
setError("No state returned from sign-in.");
return;
}

if (result.state.type === SignInState.Completed) {
setMessage("Sign-in successful!");
} else if (result.state.type === SignInState.Failed) {
if (result.error?.isInvalidUsername()) {
setError("Invalid username.");
} else if (result.error?.isUserNotFound()) {
setError("User not found.");
} else {
setError(result.error?.errorData?.message || "Unknown error.");
}
} else if (result.state.type === SignInState.PasswordRequired) {
router.push("/sign-in/password");
} else if (result.state.type === SignInState.CodeRequired) {
router.push("/sign-in/code");
} else {
setError("Unknown sign-in state.");
}
};

return (
<main>
<div className="auth-container">
<h2>Sign In</h2>
{error && <p style={{ color: "red" }}>{error}</p>}
{message && <p style={{ color: "green" }}>{message}</p>}
<button onClick={handleSignIn}>Sign In</button>
</div>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { useState } from "react";
import { useAuth } from "@/app/context/AuthContext";
import {
AuthFlowStateHandlerFactory,
SignInPasswordRequired,
SignInState,
} from "@azure/msal-custom-auth";

export default function PasswordPage() {
const { authState, setAuthState } = useAuth();
const [password, setPassword] = useState("");
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

const handleSubmitPassword = async () => {
if (authState instanceof SignInPasswordRequired) {
const handler = AuthFlowStateHandlerFactory.create(authState);
const result = await handler.submitPassword(password);
setAuthState(result.state);
if (result.state?.type === SignInState.Completed) {
setMessage(
result.data?.getAccount()?.idToken ?? "Sign-in successful!"
);
} else if (result.state?.type === SignInState.Failed) {
setError(result.error?.errorData?.message ?? "Sign-in failed.");
}
}
};

return (
<main>
<div className="auth-container">
<h1>Enter Password</h1>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button onClick={handleSubmitPassword}>Submit</button>
{error && <p style={{ color: "red" }}>{error}</p>}
{message && <p>{message}</p>}
</div>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend calling the pages AttributesPage, CodePage, PasswordPage so that the client doesn't get confused where the code goes and how it's structured.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spetrescu84 the entry point of a route cannot be renamed in nextjs, it needs a page.tsx file for routes. Read here https://nextjs.org/docs/app/getting-started/layouts-and-pages#pages

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see it can but it becomes too complex - https://nextjs.org/docs/pages/api-reference/config/next-config-js/pageExtensions. We can leave as is.


import { useState } from "react";
import { useAuth } from "@/app/context/AuthContext";
import {
AuthFlowStateHandlerFactory,
SignUpAttributesRequired,
SignUpCompleted,
SignUpState,
UserAccountAttributes,
} from "@azure/msal-custom-auth";
import router from "next/router";

export default function AttributesPage() {
const { authState, setAuthState } = useAuth();
const [displayName, setDisplayName] = useState("");
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

const handleSubmitAttributes = async () => {
if (authState instanceof SignUpAttributesRequired) {
const attributes = new UserAccountAttributes();
attributes.setDisplayName(displayName);

const handler = AuthFlowStateHandlerFactory.create(authState);
const result = await handler.submitAttributes(attributes);
setAuthState(result.state);
if (result.state instanceof SignUpCompleted) {
setMessage("Sign-up successful!");
// Sample codes for sign-in after sign-up
// const signInHandler = AuthFlowStateHandlerFactory.create(result.state);
// const signInResult = await signInHandler.signIn();
} else if (result.state?.type === SignUpState.PasswordRequired) {
router.push("/sign-up/password");
} else if (result.state?.type === SignUpState.Failed) {
if (result.error?.isMissingRequiredAttributes()) {
setError("Required attributes are missing.");
}
if (result.error?.isAttributesValidationFailed()) {
setError("Attributes validation failed.");
} else {
setError(
result.error?.errorData?.message || "Unknown error."
);
}
}
}
};

return (
<main>
<div className="auth-container">
<h1>Enter Attributes</h1>
<input
type="input"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
/>
<button onClick={handleSubmitAttributes}>Submit</button>
{error && <p style={{ color: "red" }}>{error}</p>}
{message && <p>{message}</p>}
</div>
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import { useState } from "react";
import { useAuth } from "@/app/context/AuthContext";
import {
AuthFlowStateHandlerFactory,
SignUpCodeRequired,
SignUpCompleted,
SignUpState,
} from "@azure/msal-custom-auth";
import router from "next/router";

export default function CodePage() {
const { authState, setAuthState } = useAuth();
const [code, setCode] = useState("");
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

const handleSubmitCode = async () => {
if (authState instanceof SignUpCodeRequired) {
const handler = AuthFlowStateHandlerFactory.create(authState);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this one be moved inside the sdk? So there is an authState.handler which gets returned? Even if it's not used, it's a much better user experience to just say authState.handler.submitCode?

const result = await handler.submitCode(code);
setAuthState(result.state);
if (result.state instanceof SignUpCompleted) {
setMessage("Sign-up successful!");
// Sample codes for sign-in after sign-up
// const signInHandler = AuthFlowStateHandlerFactory.create(result.state);
// const signInResult = await signInHandler.signIn();
} else if (result.state?.type === SignUpState.PasswordRequired) {
router.push("/sign-up/password");
} else if (result.state?.type === SignUpState.AttributesRequired) {
router.push("/sign-up/attributes");
} else if (result.state?.type === SignUpState.Failed) {
if (result.error?.isInvalidCode()) {
setError("Invalid password.");
} else {
setError(
result.error?.errorData?.message || "Unknown error."
);
}
}
}
};

return (
<main>
<div className="auth-container">
<h1>Enter Code</h1>
<input
type="code"
value={code}
onChange={(e) => setCode(e.target.value)}
/>
<button onClick={handleSubmitCode}>Submit</button>
{error && <p style={{ color: "red" }}>{error}</p>}
{message && <p>{message}</p>}
</div>
</main>
);
}
Loading