Sample that authenticates serverless SPA with AWS Amplify + Angular
ServerlessDay 19
WARNING: This is a Google translated (originally Chinese) and reposted article that probably is mostly comfortably understandable for senior JavaScript developers.
Until now, when attempting to realize serverless SPA on AWS, it was mainstream to use Cognito 's AWS JavaScript library called amazon-cognito-identity-js to implement the authentication part .
However, when using amazon - cognito - identity - js , it is necessary to read multiple libraries including the AWS SDK besides this library body, and some of the APIs that are prepared are not good There was a part that feels pain.
In this time, I would like to implement Cognito's authentication function in Angular application using the AWS Amplify library newly released from AWS .
For an overview of AWS Amplify, please refer to that article by nakayama_cw on the 4th day of this advent calendar with AWS Amplify in the article called building a serverless web application (Cognito + API Gateway + IAM authentication) .
Supplement
There are three packages in AWS Amplify. The base is aws-amplify , React up to the UI components around login are provided aws-amplify-react , ReactNative implementation similar to the React version What I am doing is aws - amplify - react - native .
※ The linked page, the page of npm package
What to do this time
As mentioned above, in addition to the base library, libraries including React components are available, but here I would like to try AWS Amplify even in Angluar.
This time, Authwe introduce the procedure to implement the following function in Angluar application using aws-amplify module.
- Sign up function
- Login function
- Logout function
- Transition operation by Router Guard
In addition, the completed version of the sample to be created this time is released in the following repository.
procedure
surroundings
- Angluar
$ ng -v
...
Angular CLI: 1.6.1
Node: 9.2.0
OS: darwin x64
Angular:
...
- aws-amplify
0.1.20
Resource preparation of AWS (Cognito)
Create a Cognito User Pool
Log in to the AWS Console and select Cognito -> Manage User Pool -> Create User Pool.
In "How do you create a user pool?", Select "Set according to step".
In addition, you will be shown the method of signing in the following procedure, but here, check "E-mail address and phone number".
In the access right setting to the user pool, expand the menu from "Add application client".
Enter "Application client name" and uncheck "Generate Klein and secret" to create application client.
Leave the defaults for the other items.
Create a Cognito Identity Pool
Choose Cognito -> Manage Federated Identity.
When you select it, the screen of "Create a new ID pool" is displayed. Set as follows and click "Create Pool".
For Certification provider, select Cognito and enter the user pool ID and application client ID created earlier.
Preparation of Angular project
First, I will create a project.
In this example, --routingwe have added an option to implement the transition operation by Router Guard .
$ of new angular-aws-amplify - porting
Next I will install necessary packages.
The only necessary package is AWS Amplify
$ npm install aws-amplify --save
Next, I will describe the AWS information to be passed at the time of initialization of AWS Amplify in the setting file.
This time ng new, I src/environments/environment.tswill describe to , which was created when I did.
src/environments/environment.ts
export const environment = {
production: false,
amplify: {
// 以下がAWS Amplify(Auth)の設定
Auth: {
identityPoolId: 'ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
region: 'ap-northeast-1',
userPoolId: 'ap-northeast-1_xxxxxxxxx',
userPoolWebClientId: 'xxxxxxxxxxxxxxxxxxxxxxxxx'
}
}
};
AuthService
Create a service that summarizes the processing of the authentication system.
$ of g service auth / auth
In Amplify.configure()addition to initializing the configuration file by passing it in the constructor, we will implement a method for handling the functions of the Auth module of AWS Amplify.
src/app/auth/auth.service.ts
import { Injectable } from '@angular/core';
// 追加
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { fromPromise } from 'rxjs/observable/fromPromise';
import { map, tap, catchError } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';
import Amplify, { Auth } from 'aws-amplify';
import { environment } from './../../environments/environment';
@Injectable()
export class AuthService {
public loggedIn: BehaviorSubject<boolean>;
constructor(
private router: Router
) {
Amplify.configure(environment.amplify);
this.loggedIn = new BehaviorSubject<boolean>(false);
}
/** サインアップ */
public signUp(email, password): Observable<any> {
return fromPromise(Auth.signUp(email, password));
}
/** 検証 */
public confirmSignUp(email, code): Observable<any> {
return fromPromise(Auth.confirmSignUp(email, code));
}
/** ログイン */
public signIn(email, password): Observable<any> {
return fromPromise(Auth.signIn(email, password))
.pipe(
tap(() => this.loggedIn.next(true))
);
}
/** ログイン状態の取得 */
public isAuthenticated(): Observable<boolean> {
return fromPromise(Auth.currentAuthenticatedUser())
.pipe(
map(result => {
this.loggedIn.next(true);
return true;
}),
catchError(error => {
this.loggedIn.next(false);
return of(false);
})
);
}
/** ログアウト */
public signOut() {
fromPromise(Auth.signOut())
.subscribe(
result => {
this.loggedIn.next(false);
this.router.navigate(['/login']);
},
error => console.log(error)
);
}
}
- loggedIn(BehaviorSubject)
- I use it to switch the display of global navigation.
- signUp()、confirmSignUp()、signIn()、geUserInfo()
- The Promise returned by the method of AWS Amplify Auth module of the Rx fromPromiseonly be converted to Observable in.
- isAuthenticated()
- When logged in Auth.currentAuthenticatedUser(), it returns Promise 's user information object, so we convert it to Boolean' s Observable. (For use with the Guard to be implemented later)
- signOut()
- Auth.signOut()Call up the login screen while calling.
AuthGuard
With Angular's Guard, you can perform session checking and other processing before routing to deny routing.
$ of g guard auth / auth
In this time, CanActivateusing the function of, create a Guard that makes screen transition on the login screen when not in login state.
Can
CanActivateuse Promise or a boolean resulting from asynchronous processing of Observable as a return value, so
we isAuthenticated()use the result of the method created by the AuthService earlier .
src/app/auth/auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
// 追加
import { of } from 'rxjs/observable/of';
import { map, tap, catchError } from 'rxjs/operators';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private auth: AuthService
) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> {
return this.auth.isAuthenticated()
.pipe(
tap(loggedIn => {
if (!loggedIn) {
this.router.navigate(['/login']);
}
})
);
}
}
AppRoutingModule
We will define the routing.
Since the --routingoption is specified when creating the project, src/app/app-routing.module.tsit should have been created. I will edit this file.
src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule, CanActivate } from '@angular/router';
// 追加
import { LoginComponent } from './login/login.component';
import { SignupComponent } from './signup/signup.component';
import { HomeComponent } from './home/home.component';
import { AuthGuard } from './auth/auth.guard';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard] },
{ path: 'login', component: LoginComponent },
{ path: 'signup', component: SignupComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
There are three pages, HomeComponent, LoginComponent, SignupComponent, so that only users logging in HomeComponent can view it, AuthGuardit is verified before routing.
* We will introduce the components of each page later.
LoginComponent
Create the components of the login page.
$ of g component login
src/app/login/login.component.html
login
class="login">
Email:
type="email" formControlName="email">
Password:
type="password" formControlName="password">
src/app/login/login.component.ts
import { Component, OnInit } from '@angular/core';
// 追加
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from './../auth/auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
public loginForm: FormGroup;
constructor(
private fb: FormBuilder,
private router: Router,
private auth: AuthService
) { }
byInInit () {
this . initForm ();
}
initForm() {
this.loginForm = this.fb.group({
'email': ['', Validators.required],
'password': ['', Validators.required]
});
}
onSubmitLogin(value: any) {
const email = value.email, password = value.password;
this.auth.signIn(email, password)
.subscribe(
result => {
this.router.navigate(['/']);
},
error => {
console.log(error);
});
}
}
- onSubmitLogin()
- Event handler when login form is submitted. As a result AuthServiceof the signIn()method of, if successful login will transition to home.
SignupComponent
Create a component of the signup (registration) page.
On this page, input of registration information and verification of verification code are carried out.
$ of g component signup
src/app/login/signup.component.html
signup
class="signup" *ngIf="!successfullySignup">
Email:
type="email" formControlName="email">
Password:
type="password" formControlName="password">
class="confirmation" *ngIf="successfullySignup">
Email:
type="email" formControlName="email">
Confirmation Code:
type="text" formControlName="confirmationCode">
src/app/login/signup.component.ts
import { Component, OnInit } from '@angular/core';
// 追加
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from './../auth/auth.service';
@Component({
selector: 'app-signup',
templateUrl: './signup.component.html',
styleUrls: ['./signup.component.css']
})
export class SignupComponent implements OnInit {
public signupForm: FormGroup;
public confirmationForm: FormGroup;
public successfullySignup: boolean;
constructor(
private fb: FormBuilder,
private router: Router,
private auth: AuthService
) { }
byInInit () {
this . initForm ();
}
initForm() {
this.signupForm = this.fb.group({
'email': ['', Validators.required],
'password': ['', Validators.required]
});
this.confirmationForm = this.fb.group({
'email': ['', Validators.required],
'confirmationCode': ['', Validators.required]
});
}
onSubmitSignup(value: any) {
const email = value.email, password = value.password;
this.auth.signUp(email, password)
.subscribe(
result => {
this.successfullySignup = true;
},
error => {
console.log(error);
});
}
onSubmitConfirmation(value: any) {
const email = value.email, confirmationCode = value.confirmationCode;
this.auth.confirmSignUp(email, confirmationCode)
.subscribe(
result => {
this.router.navigate(['/login']);
},
error => {
console.log(error);
});
}
}
- onSubmitSignup()
- Event handler when sign up form is submitted. As a result AuthServiceof the signUp()method, when signing up successfully, the successfullySignupflag is set to true and the validation code input form is displayed.
- onSubmitConfirmation()
- Event handler when the validation code input form is submitted. As a result AuthServiceof the confirmSignUp()method, if you sign up successfully you will transition to login.
HomeComponent
Create a homepage component that only users logged in can view.
$ of g component home
For this time, we do not write about the function after logging in so we will omit the code.
AppComponent
In AppComponent we arranged navigation to display on each page.
app.component.html
app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
// 追加
import { Subscription } from 'rxjs/Subscription';
import { AuthService } from './auth/auth.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
subscription: Subscription;
public loggedIn: boolean;
constructor (
public auth : AuthService
) { }
ngOnInit() {
this.subscription = this.auth.isAuthenticated()
.subscribe(result => {
this.loggedIn = result;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
onClickLogout() {
this.auth.signOut();
}
}
- Navigation login / logout
- AuthServiceloggedInWe received the Subject of async pipe and changed the contents to be displayed.
- byInInit ()
- I call AuthServicea isAuthenticated()method to reacquire the login state when the page is reloaded .
- onClickLogout()
- Event handler when login button is pressed.
AppModule
The final src/app/app.module.tsevent is as follows.
src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
// 追加
import { AuthService } from './auth/auth.service';
import { AuthGuard } from './auth/auth.guard';
import { LoginComponent } from './login/login.component';
import { SignupComponent } from './signup/signup.component';
import { HomeComponent } from './home/home.component';
@NgModule({
declarations: [
AppComponent,
LoginComponent,
SignupComponent,
HomeComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
],
providers: [
AuthService,
AuthGuard
],
bootstrap: [AppComponent]
})
export class AppModule { }
This completes the basic implementation.
Impression
AuthRegarding the API used this time, amazon - cognito - identity - js is a part that needs to be combined with the AWS SDK, but because AWS Amplify successfully wraps around this troublesome process well Cognito authentication was easily implemented.
Although it is a detailed point, the part where the prepared API is unified to Promise can also be likable.
I did not mention it this time, but I'd like to try it out of them.
reference