DEV Community

R
R

Posted on • Edited on

Sign in with AppleId + Angular + NestJS

Prerequisites

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

Top comments (0)