Sign in with AppleId + Angular + NestJS
R
Posted on July 24, 2020
Prerequisites
- $99.0 USD to be Enrolled as Developer in Apple
- You should create App ID and Service ID in your Apple Developer Account.
- Check Apple documentation related to Sign in with apple feature.
- Angular project.
- NestJS project.
Initial setup
Register your app in apple. For this example, I'm using https://auth.example.com
and https://auth.example.com/auth/apple
for the URL and redirectURL respectively.
Since Apple only allows HTTPS connections for the Sign-In we will require to set up a reverse proxy with a self-signed certificate to test this locally.
To generate these certificates you could use OpenSSL. Make sure that the Common Name (eg, fully qualified hostname) is auth.example.com
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout privateKey.key -out certificate.crt
Create the following folder and move the certificates under it: /certs/secrets/
and install them on your pc afterward.
I used redbird to set up the reverse proxy.
npm install --save-dev redbird
Create a proxy file on your root folder. The proxy file should look like this:
proxy.js
const proxy = require("redbird")({
port: 80,
ssl: {
http2: true,
port: 443, // SSL port used to serve registered https routes with LetsEncrypt certificate.
key: "./certs/secrets/privateKey.key",
cert: "./certs/secrets/certificate.crt",
},
});
// Angular apps
proxy.register("auth.example.com", "http://localhost:9999");
// NestJS services
proxy.register("auth.example.com/auth", "http://localhost:3333");
Run the proxy using node: node proxy.js
Note: make sure that the ports are pointing correctly for you ;)
The frontend
For the frontend project, we will install angularx-social-login. This library is a social login and authentication module for Angular 9 / 10. Supports authentication with Google, Facebook, and Amazon. Can be extended to other providers also.
npm install --save angularx-social-login
If you are lazy enough as me, you could clone the project from angularx-social-login and work from that we are just going to
extend his library to add the provider for Apple Sign in.
app.module.ts
We register the module for social login.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { NavigationComponent } from './navigation/navigation.component';
import { DemoComponent } from './demo/demo.component';
import { LogoWobbleComponent } from './logo-wobble/logo-wobble.component';
import {
SocialLoginModule,
GoogleLoginProvider,
FacebookLoginProvider,
SocialAuthServiceConfig,
} from 'angularx-social-login';
import { AppleLoginProvider } from './providers/apple-login.provider';
@NgModule({
declarations: [
AppComponent,
NavigationComponent,
DemoComponent,
LogoWobbleComponent,
],
imports: [BrowserModule, FormsModule, HttpClientModule, SocialLoginModule],
providers: [
{
provide: 'SocialAuthServiceConfig',
useValue: {
autoLogin: true,
providers: [
{
id: AppleLoginProvider.PROVIDER_ID,
provider: new AppleLoginProvider(
'[CLIENT_ID]'
),
},
],
} as SocialAuthServiceConfig,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
apple.provider.ts
The AppleLoginProvider will inherit from BaseLoginProvider of angularx-social-login library which has implemented a method to load the script required to enable the Sign In.
import { BaseLoginProvider, SocialUser } from 'angularx-social-login';
declare let AppleID: any;
export class AppleLoginProvider extends BaseLoginProvider {
public static readonly PROVIDER_ID: string = 'APPLE';
protected auth2: any;
constructor(
private clientId: string,
private _initOptions: any = { scope: 'email name' }
) {
super();
}
public initialize(): Promise<void> {
return new Promise((resolve, _reject) => {
this.loadScript(
AppleLoginProvider.PROVIDER_ID,
'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js',
() => {
AppleID.auth.init({
clientId: this.clientId,
scope: 'name email',
redirectURI: 'https://auth.example.com/auth/apple',
state: '[ANYTHING]', //used to prevent CSFR
usePopup: false,
});
resolve();
}
);
});
}
public getLoginStatus(): Promise<SocialUser> {
return new Promise((resolve, reject) => {
// todo: implement
resolve();
});
}
public async signIn(signInOptions?: any): Promise<SocialUser> {
try {
const data = await AppleID.auth.signIn()
} catch (er) {
console.log(er);
}
}
public signOut(revoke?: boolean): Promise<any> {
return new Promise((resolve, reject) => {
// AppleID doesnt have revoke method
resolve();
});
}
}
Brief explanation.
- The client id is the identifier id of the Apple APP. After creating this in your developer account you should have gotten it.
- The method
initialize()
will include the library of Apple, which already gives the objectAppleID
, required to enable the login. The important thing is thatredirectURI
has to be https. - This call
await AppleID.auth.signIn()
will initialize the Sign-in of apple after successful login it will trigger a POST request to theredirectURI
.
The backend
I'm assuming that you are familiar with NestJS so here I'm only going to show the required minimal code.
For the backend, I'm using another library to decrypt the code that is sent from apple after a successful login. So, let's go ahead and install it :D.
npm install --save apple-signin
apple-signin allows you to authenticate users using Apple account in your Node.js application.
The Apple auth controller.
apple.controller.ts
import {
Controller,
Get,
Post,
Body,
ForbiddenException,
} from '@nestjs/common';
import { AppleService } from './apple.service';
@Controller()
export class AppleController {
constructor(private readonly appleService: AppleService) {}
@Post('/apple')
public async appleLogin(@Body() payload: any): Promise<any> {
console.log('Received', payload);
if (!payload.code) {
throw new ForbiddenException();
}
return this.appleService.verifyUser(payload);
}
}
apple.service.ts
import { Injectable, ForbiddenException } from '@nestjs/common';
import * as appleSignin from 'apple-signin';
import path = require('path');
@Injectable()
export class AppleService {
public getHello(): string {
return 'Hello World dfs!';
}
public async verifyUser(payload: any): Promise<any> {
const clientSecret = appleSignin.getClientSecret({
clientID: '[CLIENT_ID]',
teamId: '[TEAM_ID]',
keyIdentifier: '[KEY_ID]',
privateKeyPath: path.join(__dirname, '/secrets/[APPLE_KEY].p8'),
});
const tokens = await appleSignin.getAuthorizationToken(payload.code, {
clientID: '[CLIENT_ID]',
clientSecret: clientSecret,
redirectUri: 'https://auth.example.com/auth/apple',
});
if (!tokens.id_token) {
console.log('no token.id_token');
throw new ForbiddenException();
}
console.log('tokens', tokens);
// TODO: AFTER THE FIRST LOGIN APPLE WON'T SEND THE USERDATA ( FIRST NAME AND LASTNAME, ETC.) THIS SHOULD BE SAVED ANYWHERE
const data = await appleSignin.verifyIdToken(tokens.id_token);
return { data, tokens };
}
}
NOTE
- All the data required on this part should have been gotten when you registered your webapp in apple. Just make sure that the private key file is available at the path
/secrets/[APPLE_KEY].p8
:
clientID: '[CLIENT_ID]',
teamId: '[TEAM_ID]',
keyIdentifier: '[KEY_ID]',
privateKeyPath: path.join(__dirname, '/secrets/[APPLE_KEY].p8'),
- Another important note is that after the first successful login, apple will not send you the User data, so this should be stored anywhere.
Enjoy! :)
Posted on July 24, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.