Diagram
Amazon Cognito Client Workflow (draw.io viewer)
In my previous blog post, “AWS CDK - Testing Amazon Cognito Authentication and Authorization”, I go-over testing the Auth Service stack, ensuring we receive back proper JWT tokens. I will now illustrate how to make use of the Auth Service stack within a hypothetical React application.
Components
Component | File Name | Description |
---|---|---|
Authentication Service | AuthService.ts | Perform user challenge and authentication and authorization, returning JWT Tokens and AWS Credentials. |
Login Component | LoginComponent.tsx | A very simple react component, which will perform the authentication, using the AuthService.ts service file. |
Frontend Router | App.tsx | A very simple React frontend web application which will present a router, with ‘/login’ making a call to LoginComponent.tsx. |
Overview
The components are stored and bundled into an S3 Bucket and served as static content. Hypothetically, in addition to the Authentication Service, APIs called via API Gateway can be used to trigger Lambda functions, which can make use of the custom scopes as injected by a Cognito Identity Pool client and/or, the ID Token used as an Authorization header to the API, whereby API Gateway can desingate Cognito as an Authorizer to check for access. We may also parse group memberships within the token to determine whether certain API methods are allowed or not.
Breaking It Down
Authentication Service
The authentication service code has been discussed at length, from the previous two blog posts, we perform a deep dive into creating a testing the auth service stack. The code here is identical to what we previously covered.
Some additional changes made to the AuthService file are as follows:
private async generateTemporaryCredentials() {
const cognitoIdentityPool = `cognito-idp.${awsRegion}.amazonaws.com/${AuthStack.SpaceUserPoolId}`;
const cognitoIdentity = new CognitoIdentityClient({
credentials: fromCognitoIdentityPool({
clientConfig: {
region: awsRegion
},
identityPoolId: AuthStack.SpaceIdentityPoolId,
logins: {
[cognitoIdentityPool]: this.jwtToken!
}
})
});
const credentials = await cognitoIdentity.config.credentials();
return credentials;
}
- The function above will use Cognito for access to the temporary AWS Credentials by supplying the JWT Auth Token. When the Web Application wants to perform a task on behalf of the user, such as uploading a photo to an S3 bucket, these credentials may be used.
- The Assumed IAM Role for the Credentials will be the Authenticated default for Cognito. Any additional Cognito groups with designated IAM roles will also provide those permissions as well.
- This is useful for delineating group permissions according to membership type, such as premium vs non-premium members.
- The Assumed IAM Role for the Credentials will be the Authenticated default for Cognito. Any additional Cognito groups with designated IAM roles will also provide those permissions as well.
Login Component
Here we break down the code used for performing the login and ensuring the user authenticated. We make extensive use of State Hooks, which provide a clean way to initialize and update states across our web app.
type LoginProps = {
authService: AuthService;
setUserNameCb: (userName: string) => void;
};
export default function LoginComponent({ authService, setUserNameCb }: LoginProps) {
const [userName, setUserName] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [errorMessage, setErrorMessage] = useState<string>("");
const [loginSuccess, setLoginSuccess] = useState<boolean>(false);
- A function labeled,
LoginComponent
is created with one argument of typeLoginProps
. We defined the ‘type’ above, via thetype LoginProps
line.- Hence, this one argument will require a type which contains two components, ‘authService’ of type ‘AuthService’, and ‘setUserNameCb’ which is a callback function that takes a string without a return.
- We then initialize four State Hooks, each empty (and boolean as false) accessible via
userName
,password
,errorMessage
, andloginSuccess
.- We can now set and use these variables by calling their ‘setIndex’ for setting the state, and referencing each variable where they each apply.
const handleSubmit = async (event: SyntheticEvent) => {
event.preventDefault();
if (userName && password) {
const loginResponse = await authService.login(userName, password);
const userName2 = authService.getUserName();
if (userName2) {
setUserNameCb(userName2);
}
if (loginResponse) {
setLoginSuccess(true);
} else {
setErrorMessage("invalid credentials");
}
} else {
setErrorMessage("UserName and password required!");
}
};
- The
handleSubmit
will take in one ‘event’ argument, a SyntheticEvent type which will be explained further below. This will grab the end-user action and perform the following:- If the user provided a username and password within the login form, the
loginResponse
variable will perform a call to theauthService.login
method, which was supplied by the ‘LoginProps’ type argument. - The method is supplied with the username and password, now being handed off to the Authentication Service component, will use Cognito to authenticate the user, then store the JWT tokens and make available the AWS Credentials.
- The ‘username2’ variable will contain the username if the
authService.getUserName()
succeeds. If set, set as the argument to thesetUserNameCb
callback supplied to the ‘LoginProps’ type argument. - If
LoginResponse
was populated from the Auth Service login function, the State Hook forloginSuccess
variable gets called,setLoginSuccess
, setting it to ‘true’.
- If the user provided a username and password within the login form, the
return (
<div role="main">
{loginSuccess && <Navigate to="/profile" replace={true} />}
<h2>Please login</h2>
<form onSubmit={(e) => handleSubmit(e)}>
<label>User name</label>
<input
value={userName}
onChange={(e) => setUserName(e.target.value)}
/>
<br/>
<label>Password</label>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
/>
<br/>
<input type="submit" value="Login" />
</form>
<br/>
{renderLoginResult()}
</div>
);
- The Login Component returns the React frontend component for logging in, the form for supplying the username and password, and a Submit button to set the variables for username and password via the Set Hooks,
setUsername
andsetPassword
.- Notice the event being supplied to the ‘handleSubmit’ function ‘event’ parameter as
e
, which carries over each of their respectivevalue
vars supplied by the form to an anonymous function, which is supplied ase.target.value
to each of the State Hooks’ ‘setIndexes’, such assetUserName(e.target.value)
. - CLicking ‘Submit’ triggers the ‘setIndex’ calls to the
handleSubmit
function.
- Notice the event being supplied to the ‘handleSubmit’ function ‘event’ parameter as
Application Router
Finally, the Application Router presents the code required for rendering a component based on the URL path.
const authService = new AuthService();
const dataService = new DataService(authService);
function App() {
const [userName, setUserName] = useState<string | undefined>(undefined);
const router = createBrowserRouter([
{
element: (
<>
<NavBar userName={userName}/>
<Outlet />
</>
),
children:[
{
path: "/",
element: <div>Hello world!</div>,
},
{
path: "/login",
element: <LoginComponent authService={authService} setUserNameCb={setUserName}/>,
},
{
path: "/profile",
element: <div>Profile page</div>,
},
{
path: "/createSpace",
element: <CreateSpace dataService={dataService}/>,
},
{
path: "/spaces",
element: <Spaces dataService={dataService}/>,
},
]
},
]);
- Here, we have a router which will perform a component depending on where the user is redirected. Their would be a navbar at the entry point, which then routes to each of the provided
path
values. Based on the ‘path’, theelement
is dynamically rendered. - For
path: "/login"
, the LoginComponent service is called as follows:LoginComponent authService={authService} setUserNameCb={setUserName}
- Notice the argument adheres to the
LoginProps
type declared in the LoginComponents above, with theauthService
being the importedAuthService()
function assigned to the variable at the top of the code, along with, setUserNameCb
, which is a callback function, called back by the LoginComponent when its username form field is set. The callback triggers the ‘setIndex’ for theuserName
State Hook:const [userName, setUserName] = useState<string | undefined>(undefined)
.- Recall the LoginComponent’s function decleration, which declared the paramters as
function LoginComponent({ authService, setUserNameCb }: LoginProps)
.setUserNameCb
in LoginComponent is the triggers thesetUserName
‘setIndex’ in App.tsx, as it’s called back.- LoginProps type declared ‘setUserNameCb’ as an anonymous function, returning void as:
setUserNameCb: (userName: string) => void
- Notice the argument adheres to the