Sign in with AppleId + Angular + NestJS

Tags: angular, javascript, tutorial, todayilearned
Published: 2020-07-24 14:05:22

## Prerequisites
- $99.0 USD to be [Enrolled as Developer in Apple](https://developer.apple.com/programs/enroll/)
- You should create App ID and Service ID in your Apple Developer Account.
- Check Apple documentation related to [Sign in with apple](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js) 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`

```typescript
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](https://github.com/abacritt/angularx-social-login#readme). 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. 

```typescript
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](https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js) required to enable the Sign In.

```typescript
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 object `AppleID`, required to enable the login. The important thing is that `redirectURI` 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 the `redirectURI`. 

## 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](https://www.npmjs.com/package/apple-signin) allows you to authenticate users using Apple account in your Node.js application.

### The Apple auth controller.

`apple.controller.ts`

```typescript
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`

```typescript
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`:

```typescript
 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! :)