Added the MSAL JS sample (#253)

This commit is contained in:
Elio Struyf 2017-07-21 19:53:19 +02:00 committed by Vesa Juvonen
parent b2b9c6ea76
commit 96936add47
30 changed files with 658 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 4
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 4

View File

@ -0,0 +1 @@
* text=auto

32
samples/react-msal-msgraph/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,14 @@
# Folders
.vscode
coverage
node_modules
sharepoint
src
temp
# Files
*.csproj
.git*
.yo-rc.json
gulpfile.js
tsconfig.json

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"version": "1.1.1",
"libraryName": "react-msal-msgraph",
"libraryId": "169d1e68-4272-4d99-ac26-e4acd43d62fd",
"environment": "spo"
}
}

View File

@ -0,0 +1,73 @@
# Microsoft Authentication Library (MSAL JS) authentication sample
## Summary
Sample SharePoint Framework web part which makes use of the [Microsoft Authentication Library (MSAL JS)](https://github.com/AzureAD/microsoft-authentication-library-for-js) to call the Microsoft Graph.
### MSAL WP
The sample web part will retrieve an access token with the `User.Read` and `Mail.Read` scope. Once an access token is retrieved, it will do a call to receive the current user and its mail messages.
![Permission scopes](./assets/permission-scopes.png)
After you gave permissions, the following will information will get displayed:
![The MSAL web part displayed in SharePoint workbench](./assets/msal-wp-output.png)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-GA-green.svg)
## Applies to
* [SharePoint Framework](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Solution
Solution|Author(s)
--------|---------
react-msal-msgraph|Elio Struyf (MVP, [U2U](https://www.u2u.be), [@eliostruyf](https://www.twitter.com/eliostruyf))
## Version history
Version|Date|Comments
-------|----|--------
1.0.0|March 17, 2017|Initial release
## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
---
## Prerequisites
- Office 365 subscription with SharePoint Online and Exchange
## Minimal Path to Awesome
- Clone this repo
- Go and register a new application on [https://apps.dev.microsoft.com](https://apps.dev.microsoft.com)
- Once logged in, click on **add an app**
- Specify the application name, and click create
- Click on **add platform**, and choose **web**
- Specify the workbench URL and be sure that **allow implicit flow** is enabled
- Click on save to store these changes
![Web URL configuration and implicit flow](./assets/redirect-url.png)
- Copy the **application id** and change add this to the [MsalWP.tsx file on line 20](./src/webparts/msalWp/components/MsalWp.tsx#20)
- Run `npm i`
- Run `gulp serve --nobrowser`
- Test out your web part in the local or hosted workbench
## Features
Sample web part in this solution illustrates the following concepts on top of the SharePoint Framework:
- using React for building SharePoint Framework client-side web parts
- using Office UI Fabric React styles for building user experience consistent with SharePoint and Office
- on-demand authentication with the Azure Active Directory using the MSAL JS library
- communicating with the Microsoft Graph using its REST API
- using the MSAL JS library with SharePoint Framework web parts built using React
![](https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-msal-msgraph)

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,16 @@
{
"entries": [{
"entry": "./lib/webparts/msalWp/MsalWpWebPart.js",
"manifest": "./src/webparts/msalWp/MsalWpWebPart.manifest.json",
"outputPath": "./dist/msal-wp.bundle.js"
}],
"externals": {
"Msal": {
"path": "https://secure.aadcdn.microsoftonline-p.com/lib/0.1.1/js/msal.min.js",
"globalName": "Msal"
}
},
"localizedResources": {
"msalWpStrings": "webparts/msalWp/loc/{locale}.js"
}
}

View File

@ -0,0 +1,3 @@
{
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,6 @@
{
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-msal-msgraph",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,10 @@
{
"solution": {
"name": "react-msal-msgraph-client-side-solution",
"id": "169d1e68-4272-4d99-ac26-e4acd43d62fd",
"version": "1.0.0.0"
},
"paths": {
"zippedPackage": "solution/react-msal-msgraph.sppkg"
}
}

View File

@ -0,0 +1,9 @@
{
"port": 4321,
"initialPage": "https://localhost:5432/workbench",
"https": true,
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,45 @@
{
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-unused-imports": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,3 @@
{
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

View File

@ -0,0 +1,34 @@
{
"name": "react-msal-msgraph",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"@microsoft/sp-core-library": "~1.1.0",
"@microsoft/sp-webpart-base": "~1.1.1",
"@types/react": "0.14.46",
"@types/react-addons-shallow-compare": "0.14.17",
"@types/react-addons-test-utils": "0.14.15",
"@types/react-addons-update": "0.14.14",
"@types/react-dom": "0.14.18",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"msal": "0.1.1",
"react": "15.4.2",
"react-dom": "15.4.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.1.0",
"@microsoft/sp-module-interfaces": "~1.1.0",
"@microsoft/sp-webpart-workbench": "~1.1.0",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0",
"gulp": "~3.9.1"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
}
}

View File

@ -0,0 +1,3 @@
export interface IMsalWpWebPartProps {
description: string;
}

View File

@ -0,0 +1,26 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "5ff21af8-04ea-478a-a640-919b29ba5ff4",
"alias": "MsalWpWebPart",
"componentType": "WebPart",
"version": "*", // The "*" signifies that the version should be taken from the package.json
"manifestVersion": 2,
/**
* This property should only be set to true if it is certain that the webpart does not
* allow arbitrary scripts to be called
*/
"safeWithCustomScriptDisabled": false,
"preconfiguredEntries": [{
"groupId": "5ff21af8-04ea-478a-a640-919b29ba5ff4",
"group": { "default": "Under Development" },
"title": { "default": "MSAL WP" },
"description": { "default": "MSAL WP description" },
"officeFabricIconFontName": "Page",
"properties": {
"description": "MSAL WP"
}
}]
}

View File

@ -0,0 +1,54 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '@microsoft/sp-webpart-base';
import * as strings from 'msalWpStrings';
import MsalWp from './components/MsalWp';
import { IMsalWpProps } from './components/IMsalWpProps';
import { IMsalWpWebPartProps } from './IMsalWpWebPartProps';
export default class MsalWpWebPart extends BaseClientSideWebPart<IMsalWpWebPartProps> {
public render(): void {
const element: React.ReactElement<IMsalWpProps > = React.createElement(
MsalWp,
{
description: this.properties.description,
context: this.context
}
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,35 @@
import { IWebPartContext } from "@microsoft/sp-webpart-base";
export interface IMsalWpProps {
description: string;
context: IWebPartContext;
}
export interface IMsalWpState {
loading?: boolean;
loggedIn?: boolean;
person?: IPerson;
mails?: IMail[];
}
export interface IPerson {
id: string;
businessPhones?: any[];
displayName?: string;
givenName?: string;
jobTitle?: string;
mail?: string;
mobilePhone?: any;
officeLocation?: any;
preferredLanguage?: string;
surname?: string;
userPrincipalName?: string;
}
export interface IMails {
value: IMail[];
}
export interface IMail {
subject: string;
}

View File

@ -0,0 +1,11 @@
.msalWp {
.container {
max-width: 700px;
margin: 0px auto;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.row {
padding: 20px;
}
}

View File

@ -0,0 +1,190 @@
/// <reference path="../../../../node_modules/msal/out/msal.d.ts" />
import * as React from 'react';
import styles from './MsalWp.module.scss';
import { IMsalWpProps, IMsalWpState, IPerson, IMails, IMail } from './IMsalWpProps';
import { HttpClient } from "@microsoft/sp-http";
import { PrimaryButton, Persona, PersonaSize, PersonaInitialsColor, List, Spinner, SpinnerType } from 'office-ui-fabric-react';
/**
* Load the MSAL library
*/
require('Msal');
declare const Msal;
/**
* MSAL Config
* Register your app here: https://apps.dev.microsoft.com/
*/
const msalconfig = {
clientID: "00000000-0000-0000-0000-000000000000", // Azure AD Application
redirectUri: location.origin
};
const scopes = ["User.Read", "Mail.Read"];
export default class MsalWp extends React.Component<IMsalWpProps, IMsalWpState> {
private clientApplication: Msal.UserAgentApplication;
constructor(props: IMsalWpProps) {
super(props);
// Set the initial state of the component
this.state = {
loading: false,
loggedIn: false,
person: null,
mails: []
};
// Initialize the user agent application for MSAL
if (!this.clientApplication) {
this.clientApplication = new Msal.UserAgentApplication(msalconfig.clientID, null, (errorDesc, token, error, tokenType) => {
// Called after loginRedirect or acquireTokenPopup
});
}
// Check if the user can be retrieved and automatically get your data
if (this.clientApplication.getUser()) {
this._getAccessToken();
}
this._login = this._login.bind(this);
this._getAccessToken = this._getAccessToken.bind(this);
this._getCrntUser = this._getCrntUser.bind(this);
this._getMessages = this._getMessages.bind(this);
}
/**
* Function that will login the user and call the Microsoft Graph
*/
private _login(): void {
// Login the user
if (this.clientApplication.getUser()) {
this._getAccessToken();
} else {
this.clientApplication.loginPopup(scopes).then((idToken: string) => {
this._getAccessToken();
});
}
}
/**
* Retrieve an accessToken for the Microsoft Graph
*/
private _getAccessToken(): void {
this.state = {
loading: true
};
// Retrieve a accessToken to call the Microsoft Graph
this.clientApplication.acquireTokenSilent(scopes).then((token: string) => {
this._getCrntUser(token);
this._getMessages(token);
}, (error) => {
// Interaction required
if (error) {
this.clientApplication.acquireTokenPopup(scopes).then((token: string) => {
this._getCrntUser(token);
this._getMessages(token);
}, (err: string) => {
// Something went wrong
console.log('Error:', err);
this.setState({
loading: false
});
});
}
});
}
/**
* Call the current user via the Microsoft Graph
*/
private _getCrntUser(token: string): void {
// Call the Microsoft Graph
this.props.context.httpClient.get('https://graph.microsoft.com/v1.0/me', HttpClient.configurations.v1, {
headers: {
"authorization": `Bearer ${token}`
}
}).then(response => {
return response.json();
}).then((data: IPerson) => {
this.setState({
loggedIn: true,
person: data,
loading: false
});
});
}
/**
* Get the mails of the current user
*/
private _getMessages(token: string): void {
// Call the Microsoft Graph
this.props.context.httpClient.get('https://graph.microsoft.com/v1.0/me/messages', HttpClient.configurations.v1, {
headers: {
"authorization": `Bearer ${token}`
}
}).then(response => {
return response.json();
}).then((data: IMails) => {
this.setState({
mails: data.value,
loading: false
});
});
}
/**
* Component render
*/
public render(): React.ReactElement<IMsalWpProps> {
if (this.state.loading) {
return <Spinner type={ SpinnerType.large } label="Retrieving your data" />;
}
const loginBtn: JSX.Element = this.state.loggedIn ? <div /> : <PrimaryButton text='Login' onClick={this._login} />;
let userInfo: JSX.Element = <div />;
if (this.state.loggedIn && this.state.person) {
userInfo = <Persona
primaryText={this.state.person.displayName}
imageInitials={`${this.state.person.givenName.slice(0, 1)}${this.state.person.surname.slice(0, 1)}`}
secondaryText={this.state.person.jobTitle}
optionalText="w00t"
tertiaryText={this.state.person.mail}
size={PersonaSize.extraLarge}
hidePersonaDetails={false}
initialsColor={PersonaInitialsColor.red}
/>;
}
let mails: JSX.Element = <div />;
if (this.state.mails.length > 0) {
mails = <p className="ms-font-l ms-fontColor-neutralPrimary">Your mails</p>;
}
return (
<div className={styles.msalWp}>
<div className={styles.container}>
<div className={`ms-Grid-row ms-bgColor-neutralLight ms-fontColor-neutralPrimary ${styles.row}`}>
<div className="ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1">
<p className="ms-font-xl ms-fontColor-neutralPrimary">SPFx WP sample with the MSAL library</p>
{loginBtn}
{userInfo}
{mails}
<List items={this.state.mails} onRenderCell={(item: IMail, index: number) => (
<div>{index++}: {item.subject}</div>
)} />
</div>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IMsalWpStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'msalWpStrings' {
const strings: IMsalWpStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('MsalWpWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}

View File

@ -0,0 +1,11 @@
// Type definitions for Microsoft ODSP projects
// Project: ODSP
/* Global definition for UNIT_TEST builds
Code that is wrapped inside an if(UNIT_TEST) {...}
block will not be included in the final bundle when the
--ship flag is specified */
declare const UNIT_TEST: boolean;
/* Global defintion for SPO builds */
declare const DATACENTER: boolean;

View File

@ -0,0 +1 @@
/// <reference path="@ms/odsp.d.ts" />