LoginSignup
5
6

More than 3 years have passed since last update.

SharePointで作られた社内ポータルをAngular+GraphAPIでステキに閲覧する①

Last updated at Posted at 2019-06-16

会社のポータルサイトがOffice365のSharePointで構築されているのですが、どうにも見づらいのでAngular8 + Graph APIで独自に構築してみることにしました。
GraphAPIを使うまでの道のりを残しておこうと思います。

長くなりそうなので、今回は何回かに分けて書いていきます。
続き⇒SharePointで作られた社内ポータルをAngular+GraphAPIでステキに閲覧する②

なお、APIを使って色々やる場合、以下のようなフローとなるそうなので、今回は①、②の部分を書きます。

image.png
※この図はAzureでアプリ登録後のクイックスタートで確認できます

Azureでアプリを登録する

アプリ登録

  • 「すべてのサービス⇒ID⇒アプリの登録」を選択
    image.png

  • 「新規登録」押下

  • アプリ名を入力し、登録

※概要でクライアントIDテナントIDが確認できます。
 Angularへの組み込みが必要になりますので、メモっておいてください。
image.png

各種設定

リダイレクトURLを設定

  • 「認証」からリダイレクトURLを設定

image.png
※ローカル環境で動かす場合は「localhost:4200」を登録
 ⇒localhost以外はhttps必須みたいです。
 また、複数件登録できるので、localhost、共用開発環境、本番環境といった登録ができます。
 ⇒Angular側で指定するリダイレクトURLで切り分けられると思います。

暗黙的な許可フローの許可

ログインやアクセストークンの取得を暗黙的に実行したい場合はチェックを入れて保存します

image.png

Angularでフロントエンドの構築

Azureにクイックスタートがありますが、Angular向けに「@azure/msal-angular」というパッケージが用意されているので、以降はそちらの導入手順に従って実装していきます。

Angularバージョンは以下で進めていきます。

image.png

テンプレートプロジェクトの生成

以下のコマンドでテンプレートを生成します

ng new sp-portal --routing

※ルーティング欲しいので--routingオプション付けてます

msal-angularのインストール

クイックスタートはExpress.jsによるJavascriptコードで面倒くさいので、msal-angularをインストールします

npm install @azure/msal-angular --save

必要なパッケージをインストール

msal-angularはrxjs-compatを使うみたいなので、インストールします。

npm install --save rxjs-compat

コードの組み込み

ここからは@azure/msal-angularのページを参考に組み込んでいきます

AppModuleでMsalModuleをインポート

ここで、Azureに登録したアプリのクライアントIDとテナントID、リダイレクトURLを指定します。
※リダイレクトURLはログインやログアウト完了後にリダイレクトするURL
 Azure側で設定したURLと一致させる必要があります。

app.module.ts
import { MsalModule } from '@azure/msal-angular';
import { environment } from "../environments/environment";
@NgModule({
  ・・・
  imports: [
    ・・・
    MsalModule.forRoot({
      clientID: environment.msal.clientID,
      authority: environment.msal.authority,
      redirectUri: environment.msal.redirectUrl,
    })
  ]
})
export class AppModule { }

※設定できる項目はこちらを参照

なお、今回は各種設定値をenvironments.tsに切り出しています。

environment/environment.ts
export const environment = {
  production: false,
  msal: {
    clientID: "{Your Client ID}",
    authority: "https://login.microsoftonline.com/{Your Tenant ID}",
    redirectUrl: "http://localhost:4200/",
  }
};

ルーティング設定

app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MsalGuard } from '@azure/msal-angular';
import { LoginComponent } from './login/login.component';

const routes: Routes = [
  { path: 'login', component: LoginComponent },
  // Angular8のDynamic Imports
  { path: 'pages', canActivate: [MsalGuard], loadChildren: () => import('./pages/pages.module').then(m => m.PagesModule) },
  { path: '', redirectTo: 'login', pathMatch: 'full' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

path: 'pages'canActive:'MsalGuard'を指定しているため、/pages~のURLにアクセスする際にMsalGuardによるチェックが走り、認証できていない場合は強制的にOffice365のログイン画面にリダイレクトされます。

ちなみに、今回は↓のようなディレクトリ構成にしてます。

└─src
    │  
    ├─app
    │  │  app-routing.module.ts
    │  │  app.component.html
    │  │  app.component.ts
    │  │  app.module.ts
    │  │  
    │  ├─login
    │  │      login.component.ts
    │  │      
    │  └─pages
    │      │  pages-routing.module.ts
    │      │  pages.module.ts
    │      │  
    │      ├─detail
    │      │      detail.component.ts
    │      │      
    │      └─top
    │              top.component.ts
    │              
    ├─environments
    │      environment.prod.ts
    │      environment.ts
    │      
    └─services
            auth.service.ts

画面遷移イメージ

今回は、ヘッダにログイン/ログアウトボタンを置いて、任意のタイミングで認証できるようにしたいと思います。
Angular Materialflex-layoutを使って画面構築してます。
image.png
 ↓ ログイン押下
image.png
※2段階認証にも対応してます
 ↓ ログイン後
image.png
※ログイン後はログインボタンはログイン中ユーザーのアドレスを表示し、クリックするとメニューが開いてログアウトができます。
image.png

各種コンポーネント

app.component

ここでは、主にヘッダ部分の制御を記載しています。

src/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { User } from 'msal';
import { AuthService } from 'src/services/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent implements OnInit {
  /** ログイン中ユーザー情報 */
  user: User = null;

  constructor(
    private router: Router,
    private authService: AuthService,
  ) { }

  ngOnInit() {
    // ユーザー取得
    this.user = this.authService.getUser();
  }

  /**
   * ログインチェック
   */
  get isLogin() {
    return this.user ? true: false;
  }

  /**
   * タイトル押下
   */
  OnClickTitle() {
    if (this.isLogin) {
      this.router.navigate(['/pages']);
    }
  }

  /**
   * ログイン押下
   */
  async OnClickLogin() {
    // ログインポップアップ表示
    const token = await this.authService.showLoginPopup();
    if (token) {
      // ユーザー情報を取得
      this.user = this.authService.getUser();
      // TOP画面に遷移
      this.router.navigate(['/pages']);
    } else {
      this.user = null;
      alert('ログイン失敗');
    }
  }

  /**
   * ログアウト押下
   */
  async OnClickLogout() {
    this.user = null;
    // ログアウト
    this.authService.logout();
  }

}
app.component.html
<mat-toolbar fxLayout="row" fxLayoutAlign="space-between" color="primary">
    <div fxFlexAlign="center">
        <button mat-flat-button color="primary" (click)="OnClickTitle()"><h1>Title</h1></button>
    </div>
    <div fxFlexAlign="center"></div>
    <div fxFlexAlign="center">
        <button mat-raised-button color="accent" *ngIf="!isLogin" (click)="OnClickLogin()">ログイン</button>
        <button [matMenuTriggerFor]="menu" *ngIf="isLogin" mat-raised-button color="primary">{{user.displayableId}}</button>
        <mat-menu #menu="matMenu">
            <button mat-menu-item (click)="OnClickLogout()">
                <mat-icon>exit_to_app</mat-icon>
                <span>ログアウト</span>
            </button>
        </mat-menu>
    </div>
</mat-toolbar>
<router-outlet></router-outlet>

login.component.ts

ログイン済みかチェックし、ログイン済みであれば/pagesに遷移させます。

src/app/login.compoonent.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from 'src/services/auth.service';

@Component({
  selector: 'app-login',
  template: '<span>ログインしてください</span>',
})
export class LoginComponent implements OnInit {
  constructor(
    private router: Router,
    private authService: AuthService,
  ) { }

  ngOnInit() {
    if (this.authService.getUser()) {
      // ログイン済みの場合はTOPページに遷移
      this.router.navigate(['/pages']);
    }
  }
}

src/app/pages下のcomponent

今回はログインがメインなので、暫定の画面です

src/app/pages/top/top.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-pages-top',
  template: `
  <h1>Top Component</h1>
  <ul>
    <li><a routerLink="/pages/detail/1">ID1のページを表示</a></li>
    <li><a routerLink="/pages/detail/2">ID2のページを表示</a></li>
  </ul>
  `,
})
export class TopComponent {
}
src/app/pages/detail/detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';

@Component({
  selector: 'app-pages-detail',
  template: `
  <h1>Detail Component</h1>
  <span>ID:{{id}}のページです</span>
  `,
})
export class DetailComponent implements OnInit {
  id: number;

  constructor(private ar: ActivatedRoute) {}

  ngOnInit() {
    // IDを取得
    this.ar.params.subscribe((params: Params) => {
      if (params['id']) {
        this.id = params['id'];
      } else {
        alert('idを指定してください')
      }
    })
  }
}

サービスの登録

サービスの作成

ログイン/ログアウト、ユーザー情報取得処理を定義しています。
なお、サンプルでもユーザー情報の有無でログインしているかチェックしていました。

src/services/auth.service.ts
import { Injectable } from '@angular/core';
import { MsalService } from '@azure/msal-angular';

@Injectable({
    providedIn: 'root',
})
export class AuthService {
    constructor(private msalService: MsalService) { }

    /**
     * ユーザー取得
     */
    getUser() {
        return this.msalService.getUser();
    }

    /**
     * ログインポップアップ表示
     */
    async showLoginPopup() {
        try {
            return await this.msalService.loginPopup();
        } catch (err) {
            console.log(err);
            return null;
        }
    }

    /**
     * ログアウト
     */
    logout() {
        this.msalService.logout();
    }
}

SharePointAPI用のサービスを作成
※今回はまだ空です。

src/services/sharepoint-api.service.ts
import { Injectable } from '@angular/core';

@Injectable({
    providedIn: 'root',
})
export class SharePointApiService {

}

プロバイダーにサービスを登録

TTP_INTERCEPTORSMsalInterceptorを指定することで指定のサービスを呼び出す際に認証チェックが走り、認証できていない場合はOffice365のログイン画面にリダイレクトするようになります。

src/app/app.module.ts
import { MsalModule, MsalInterceptor } from '@azure/msal-angular';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { SharePointApiService } from 'src/services/sharepoint-api.service';
@NgModule({
  ・・・
  providers: [
    SharePointApiService,
    { provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true }
  ],
})
export class AppModule { }

AuthServiceは認証チェック不要な処理しか置かないので、providersには登録してません。
 登録してしまうとgetUser読んだだけでログイン画面に勝手に飛んでしまうので。。。

サーバー起動

これで準備は完了です!
以下のコマンドで起動します。

ng serve

ブラウザでhttp://localhost:4200にアクセスします
image.png

まとめ

無事にログインまではできました。
次のアクセストークン取得は別途記事に起こしたいと思います。

TIPS

ログイン周りでいくつかはまったところがあったので、書いておきます。
※上記手順には反映済みです。

AADSTS700054: response_type 'id_token' is not enabled for the application.

image.png

Azureの「アプリの登録⇒認証」からIDトークンにチェックを入れる

image.png

※一度キャッシュを消すか、シークレットウィンドウで開く

AADSTS500113: No reply address is registered for the application.

image.png

Azureの「アプリの登録⇒認証」でリダイレクトURLを選択する

image.png

※ここで指定したURLとAppModuleで指定したredirectUriが一致しないとエラーになる

AADSTS700051: response_type 'token' is not enabled for the application.

acquireTokenSilentでアクセストークンを取得しようとした際に発生
マニフェストで暗黙的なトークン取得を許可する必要があるらしい
参考:https://community.dynamics.com/crm/b/akmscrmblog/archive/2016/03/16/response-type-token-is-not-enabled-for-the-application

image.png
oauth2AllowImplicitFlowをtrueに変更する

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6