diff --git a/public/docs/_examples/cb-azure-ad-auth/e2e-spec.ts b/public/docs/_examples/cb-azure-ad-auth/e2e-spec.ts new file mode 100644 index 0000000000..8df56bc768 --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/e2e-spec.ts @@ -0,0 +1,15 @@ +/// <reference path='../_protractor/e2e.d.ts' /> +'use strict'; +describe('Azure AD Auth E2E tests', function () { + + let expectedMsg = 'Simple app demonstrates'; + + beforeEach(function () { + browser.get(''); + }); + + it(`should display: ${expectedMsg}`, function () { + expect(element(by.css('p')).getText()).toContain(expectedMsg); + }); + +}); diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/.gitignore b/public/docs/_examples/cb-azure-ad-auth/ts/.gitignore new file mode 100644 index 0000000000..5df8a1e836 --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/.gitignore @@ -0,0 +1 @@ +authSettings.config.ts \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/app.component.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/app.component.ts new file mode 100644 index 0000000000..cc38f89bf4 --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/app.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +@Component({ + selector: 'app', + template: ` + <a [routerLink]="['']">About</a> | <a [routerLink]="['login']">Login</a> | <a [routerLink]="['status']">Status</a> <br/> + <router-outlet></router-outlet>` +}) + +export class AppComponent { } \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/app.module.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/app.module.ts new file mode 100644 index 0000000000..268ec4c49f --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/app.module.ts @@ -0,0 +1,39 @@ +import { NgModule } from '@angular/core'; +import { HttpModule } from '@angular/http'; +import { BrowserModule } from '@angular/platform-browser'; +import { routing } from './app.routing'; +import { AppComponent } from './app.component'; + +import { HomeComponent } from './home/home.component'; +import { LoginComponent } from './login/login.component'; +import { StatusComponent } from './status/status.component'; + +import {AzureADServiceConstants} from './ngAuth/authenticators/AzureADServiceConstants'; +import {AzureADAuthService} from './ngAuth/authenticators/AzureADAuthService'; +import {AuthenticatedHttpService} from './ngAuth/AuthenticatedHttpService'; + +import {serviceConstants} from './authsettings.config'; + +let authenticator = new AzureADAuthService(serviceConstants); + +@NgModule({ + providers: [ + AuthenticatedHttpService, + { + provide: AzureADAuthService, + useValue: authenticator + }], + imports: [ + routing, + BrowserModule, + HttpModule + ], + declarations: [ + AppComponent, + HomeComponent, + LoginComponent, + StatusComponent + ], + bootstrap: [AppComponent] +}) +export class AppModule { } \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/app.routing.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/app.routing.ts new file mode 100644 index 0000000000..22693909b6 --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/app.routing.ts @@ -0,0 +1,12 @@ +import { ModuleWithProviders } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; +import { LoginComponent } from './login/login.component'; +import {HomeComponent} from './home/home.component'; +import { StatusComponent } from './status/status.component'; + +export const routes: Routes = [ + { path: '', component: HomeComponent }, + { path: 'login', component: LoginComponent }, + { path: 'status', component: StatusComponent }, +]; +export const routing: ModuleWithProviders = RouterModule.forRoot(routes); \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/home/home.component.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/home/home.component.ts new file mode 100644 index 0000000000..d317ce8bc4 --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/home/home.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; + +@Component({ + template:'<p>Simple app demonstrates logging into AzureAD/Office365 and running a command against the MS graph</p>' +}) +export class HomeComponent { } \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/login/login.component.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/login/login.component.ts new file mode 100644 index 0000000000..150fde24f9 --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/login/login.component.ts @@ -0,0 +1,21 @@ +import { Component, Inject } from '@angular/core'; +import { Router } from '@angular/router'; + +import {AzureADAuthService} from '../ngAuth/authenticators/AzureADAuthService'; + +@Component({ + template: ` + <button (click)="logIn()"> + Sign In + </button>` +}) + +export class LoginComponent { + constructor( + @Inject(AzureADAuthService) private _authService: AzureADAuthService, + private _router: Router) { } + + logIn() { + this._authService.logIn("/"); + } +} \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/main.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/main.ts new file mode 100644 index 0000000000..6801c0441c --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/main.ts @@ -0,0 +1,3 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import {AppModule} from './app.module'; +platformBrowserDynamic().bootstrapModule(AppModule); \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/AuthenticatedHttpService.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/AuthenticatedHttpService.ts new file mode 100644 index 0000000000..5bcb744c5c --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/AuthenticatedHttpService.ts @@ -0,0 +1,32 @@ +import { Injectable, Inject } from '@angular/core'; +import { Http, Headers, Response } from '@angular/http'; +import { AzureADAuthService } from './authenticators/AzureADAuthService'; +import { Observable, Subscriber } from 'rxjs'; + +@Injectable() +export class AuthenticatedHttpService { + private _authenticator: AzureADAuthService; + private _http: Http; + constructor( @Inject(Http) http: Http, @Inject(AzureADAuthService) authenticator: AzureADAuthService) { + this._authenticator = authenticator; + this._http = http; + } + + createAuthorizationHeader(headers: Headers) { + headers.append('Authorization', 'Bearer ' + this._authenticator.getAccessToken()); + } + + get(url: string) { + let headers = new Headers(); + this.createAuthorizationHeader(headers); + return this._http.get(url, { headers: headers }); + } + + post(url: string, data: any) { + let headers = new Headers(); + this.createAuthorizationHeader(headers); + return this._http.post(url, data, { + headers: headers, + }); + } +} \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/JwtHelper.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/JwtHelper.ts new file mode 100644 index 0000000000..f8f4a595ed --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/JwtHelper.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class JwtHelper { + private urlBase64Decode(str: string) { + var output = str.replace(/-/g, '+').replace(/_/g, '/'); + switch (output.length % 4) { + case 0: { break; } + case 2: { output += '=='; break; } + case 3: { output += '='; break; } + default: { + throw 'Illegal base64url string!'; + } + } + return decodeURIComponent((<any>window).escape(window.atob(output))); + } + + public decodeToken(token: string = "") { + if (token === null || token === "") return {"upn": ""}; + var parts = token.split('.'); + if (parts.length !== 3) { + throw new Error('JWT must have 3 parts'); + } + var decoded = this.urlBase64Decode(parts[1]); + if (!decoded) { + throw new Error('Cannot decode the token'); + } + return JSON.parse(decoded); + } +} \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/authenticators/AzureADAuthService.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/authenticators/AzureADAuthService.ts new file mode 100644 index 0000000000..4d386cc0a0 --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/authenticators/AzureADAuthService.ts @@ -0,0 +1,94 @@ +import { Injectable } from '@angular/core'; +import { Http, Headers } from '@angular/http'; +import { JwtHelper } from '../JwtHelper'; + +import { AzureADServiceConstants } from "./AzureADServiceConstants"; + +@Injectable() +export class AzureADAuthService { + public isUserAuthenticated(): boolean { + let access_token = this.getAccessToken(); + return access_token != null; + } + + public getAccessToken(): string { + return window.localStorage.getItem("access_token"); + } + + public getUserName(): string { + var jwtHelper = new JwtHelper(); + var parsedToken = jwtHelper.decodeToken(this.getAccessToken()); + + var expiryTime = new Date(parsedToken.exp * 1000); + var now = new Date(); + if (now > expiryTime) this.logOut(); + + return parsedToken.upn; + } + + public logIn(state = "/") { + window.location.href = "https://login.microsoftonline.com/" + this._serviceConstants.tenantID + + "/oauth2/authorize?response_type=id_token&client_id=" + this._serviceConstants.clientID + + "&redirect_uri=" + encodeURIComponent(window.location.href) + + "&state=" + state + "&nonce=SomeNonce"; + } + + public logOut(state = "/") { + window.localStorage.removeItem("id_token"); + window.localStorage.removeItem("access_token"); + window.location.href = state; + } + + + private parseQueryString = function (url: string) { + var params = {}; + var queryString = ""; + if (url.search("#") != -1) { + queryString = url.substring(url.search("#") + 1); + + } else { + queryString = url.substring(url.indexOf("?") + 1); + } + var a = queryString.split('&'); + for (var i = 0; i < a.length; i++) { + var b = a[i].split('='); + params[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ''); + } + return params; + } + + private params = this.parseQueryString(location.hash); + + constructor(private _serviceConstants: AzureADServiceConstants) { + // do we have an access token, if so add the iframe renewer + if (window.localStorage.getItem("access_token")) { + var iframe = document.createElement('iframe'); + iframe.style.display = "none"; + iframe.src = "/app/ngAuth/renewToken.html?tenantID=" + + encodeURIComponent(this._serviceConstants.tenantID) + + "&clientID=" + encodeURIComponent(this._serviceConstants.clientID) + + "&resource=" + encodeURIComponent(this._serviceConstants.graphResource); + window.onload = function () { + document.body.appendChild(iframe); + } + } + if (this.params["id_token"] != null) { + window.localStorage.setItem("id_token", this.params["id_token"]); + // redirect to get access token here.. + window.location.href = "https://login.microsoftonline.com/" + this._serviceConstants.tenantID + + "/oauth2/authorize?response_type=token&client_id=" + this._serviceConstants.clientID + + "&resource=" + this._serviceConstants.graphResource + + "&redirect_uri=" + encodeURIComponent(window.location.href) + + "&prompt=none&state=" + this.params["state"] + "&nonce=SomeNonce"; + } + else if (this.params["access_token"] != null) { + window.localStorage.setItem("access_token", this.params["access_token"]); + // redirect to the original call URl here. + window.location.href = this.params["state"]; + } + } +} + +function error(err: any) { + console.error(JSON.stringify(err, null, 4)); +} \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/authenticators/AzureADServiceConstants.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/authenticators/AzureADServiceConstants.ts new file mode 100644 index 0000000000..04a2e3fa22 --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/authenticators/AzureADServiceConstants.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class AzureADServiceConstants { + constructor( + public clientID:string, + public tenantID:string, + public redirectURL:string, + public backendUrl:string, + public graphResource = "https://graph.microsoft.com", + public isCordova = false, + public isElectron = false) {} +} \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/renewToken.html b/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/renewToken.html new file mode 100644 index 0000000000..de71df64bf --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/ngAuth/renewToken.html @@ -0,0 +1,58 @@ +<html> + +<head> + <title>Renew Token</title> + <script> + // jwt_decode from https://raw.githubusercontent.com/auth0/jwt-decode/master/build/jwt-decode.min.js + !function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(a,b,c){function d(a){this.message=a}function e(a){var b=String(a).replace(/=+$/,"");if(b.length%4==1)throw new d("'atob' failed: The string to be decoded is not correctly encoded.");for(var c,e,g=0,h=0,i="";e=b.charAt(h++);~e&&(c=g%4?64*c+e:e,g++%4)?i+=String.fromCharCode(255&c>>(-2*g&6)):0)e=f.indexOf(e);return i}var f="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";d.prototype=new Error,d.prototype.name="InvalidCharacterError",b.exports="undefined"!=typeof window&&window.atob||e},{}],2:[function(a,b,c){function d(a){return decodeURIComponent(e(a).replace(/(.)/g,function(a,b){var c=b.charCodeAt(0).toString(16).toUpperCase();return c.length<2&&(c="0"+c),"%"+c}))}var e=a("./atob");b.exports=function(a){var b=a.replace(/-/g,"+").replace(/_/g,"/");switch(b.length%4){case 0:break;case 2:b+="==";break;case 3:b+="=";break;default:throw"Illegal base64url string!"}try{return d(b)}catch(c){return e(b)}}},{"./atob":1}],3:[function(a,b,c){"use strict";var d=a("./base64_url_decode");b.exports=function(a){if(!a)throw new Error("Invalid token specified");return JSON.parse(d(a.split(".")[1]))}},{"./base64_url_decode":2}],4:[function(a,b,c){var d="undefined"!=typeof self?self:"undefined"!=typeof window?window:{},e=a("./lib/index");"function"==typeof d.window.define&&d.window.define.amd?d.window.define("jwt_decode",function(){return e}):d.window&&(d.window.jwt_decode=e)},{"./lib/index":3}]},{},[4]); + </script> + + <script> + 'use strict'; + var parsedToken = jwt_decode(window.localStorage.getItem("access_token")); + var timeoutMiliSeconds = (parsedToken.exp*1000 - 300000) - (new Date().getTime()); + // parse QueryString + var params = {}; + var queryString = "" ; + if (window.location.href.search("#") != -1) { + queryString = window.location.href.substring(window.location.href.search("#") + 1); + + } else { + queryString = window.location.search.substring(1); + } + var a = queryString.split('&'); + for (var i = 0; i < a.length; i++) { + var b = a[i].split('='); + params[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ''); + } + + if (params["id_token"] != null) { + window.localStorage.setItem("id_token", this.params["id_token"]); + // redirect to get access token here.. + window.location.href = "https://login.microsoftonline.com/" + params["tenantID"] + + "/oauth2/authorize?response_type=token&client_id=" + params["clientID"] + + "&resource=" + params["resource"] + + "&redirect_uri=" + encodeURIComponent(window.location.href) + + "&prompt=none&state=" + params["state"] + "&nonce=SomeNonce"; + } + else if (params["access_token"] != null) { + window.localStorage.setItem("access_token", this.params["access_token"]); + window.location.href = params["state"]; + } else { + window.setTimeout(function() { + var idToken = window.localStorage.getItem("id_token"); + var urlWithoutQueryString = window.location.href.substring(0, window.location.href.length - window.location.search.length) + window.location.href = "https://login.microsoftonline.com/" + params["tenantID"] + + "/oauth2/authorize?response_type=token&client_id=" + params["clientID"] + + "&resource=" + params["resource"] + + "&redirect_uri=" + encodeURIComponent(window.location.href) + + "&prompt=none&state=" + encodeURIComponent(window.location.href) + "&nonce=SomeNonce"; + }, timeoutMiliSeconds); + } + </script> +</head> + +<body> +</body> + +</html> \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/app/status/status.component.ts b/public/docs/_examples/cb-azure-ad-auth/ts/app/status/status.component.ts new file mode 100644 index 0000000000..c9c92791ea --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/app/status/status.component.ts @@ -0,0 +1,44 @@ +import { Component, Inject } from '@angular/core'; +import { Router } from '@angular/router'; + +import { AuthenticatedHttpService } from '../ngAuth/AuthenticatedHttpService'; +import { AzureADAuthService } from '../ngAuth/authenticators/AzureADAuthService'; + + +@Component({ + template: ` + <div *ngIf="this._authService.isUserAuthenticated()"> + userName: {{this._authService.getUserName()}} + <br/> + <button (click)="logOut()">Logout</button> + <br/> + <button (click)="runCommand()"> + Run Command + </button> + <ul> + <li *ngFor="let file of _files"> + <span>Name: {{file.remoteItem.name}}</span> + <span>Size: {{file.size}}</span> + </li> + </ul> + </div> + <div *ngIf="!this._authService.isUserAuthenticated()"> + User is not signed in. + </div> + ` +}) + +export class StatusComponent { + private _files: any[] = []; + constructor( @Inject(AzureADAuthService) private _authService: AzureADAuthService, private _authenticatedHttpService: AuthenticatedHttpService) { } + + logOut() { + this._authService.logOut("/"); + } + + runCommand() { + this._authenticatedHttpService.get("https://graph.microsoft.com/v1.0/me/drive/recent").subscribe((results => { + this._files = results.json().value; + })); + } +} \ No newline at end of file diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/example-config.json b/public/docs/_examples/cb-azure-ad-auth/ts/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/index.html b/public/docs/_examples/cb-azure-ad-auth/ts/index.html new file mode 100644 index 0000000000..836a629352 --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/index.html @@ -0,0 +1,23 @@ +<html> + <head> + <base href="/"/> + <title>Angular 2 AzureAD/Office365 Auth</title> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <!-- 1. Load libraries --> + <!-- Polyfill(s) for older browsers --> + <script src="node_modules/core-js/client/shim.min.js"></script> + <script src="node_modules/zone.js/dist/zone.js"></script> + <script src="node_modules/reflect-metadata/Reflect.js"></script> + <script src="node_modules/systemjs/dist/system.src.js"></script> + <!-- 2. Configure SystemJS --> + <script src="systemjs.config.js"></script> + <script> + System.import('app/main').catch(function(err){ console.error(err); }); + </script> + </head> + <!-- 3. Display the application --> + <body> + <app>Loading...</app> + </body> +</html> diff --git a/public/docs/_examples/cb-azure-ad-auth/ts/plnkr.json b/public/docs/_examples/cb-azure-ad-auth/ts/plnkr.json new file mode 100644 index 0000000000..a4449b444f --- /dev/null +++ b/public/docs/_examples/cb-azure-ad-auth/ts/plnkr.json @@ -0,0 +1,9 @@ +{ + "description": "QuickStart", + "files": [ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[1].*" + ], + "tags": ["quickstart"] +} \ No newline at end of file diff --git a/public/docs/ts/latest/cookbook/_data.json b/public/docs/ts/latest/cookbook/_data.json index 7a45a91e77..2c56cc3c33 100644 --- a/public/docs/ts/latest/cookbook/_data.json +++ b/public/docs/ts/latest/cookbook/_data.json @@ -4,65 +4,57 @@ "navTitle": "Overview", "description": "A collection of recipes for common Angular application scenarios" }, - "aot-compiler": { "title": "Ahead-of-Time Compilation", "intro": "Learn how to use Ahead-of-time compilation" }, - "a1-a2-quick-reference": { "title": "Angular 1 to 2 Quick Reference", "navTitle": "Angular 1 to 2 Quick Ref", "intro": "Learn how Angular 1 concepts and techniques map to Angular 2" }, - "ngmodule-faq": { "title": "Angular Module FAQs", "intro": "Answers to frequently asked questions about @NgModule" }, - + "azure-ad-auth": { + "title": "AzureAD Authentication", + "intro": "Explains how to use Angular to authenticate with AzureAD and Office365" + }, "component-communication": { "title": "Component Interaction", "intro": "Share information between different directives and components" }, - "component-relative-paths": { "title": "Component-relative Paths", "intro": "Use relative URLs for component templates and styles." }, - "dependency-injection": { "title": "Dependency Injection", "intro": "Techniques for Dependency Injection" }, - "dynamic-form": { "title": "Dynamic Forms", "intro": "Render dynamic forms with FormGroup" }, - "form-validation": { "title": "Form Validation", "intro": "Validate user's form entries" }, - "i18n": { "title": "Internationalization (i18n)", "intro": "Translate the app's template text into multiple languages" }, - "set-document-title": { "title": "Set the Document Title", "intro": "Setting the document or window title using the Title service." }, - "ts-to-js": { "title": "TypeScript to JavaScript", "intro": "Convert Angular TypeScript examples into ES5 JavaScript" }, - "visual-studio-2015": { "title": "Visual Studio 2015 QuickStart", "intro": "Use Visual Studio 2015 with the QuickStart files" } -} +} \ No newline at end of file diff --git a/public/docs/ts/latest/cookbook/azure-ad-auth.jade b/public/docs/ts/latest/cookbook/azure-ad-auth.jade new file mode 100644 index 0000000000..05ca93eab1 --- /dev/null +++ b/public/docs/ts/latest/cookbook/azure-ad-auth.jade @@ -0,0 +1,13 @@ +include ../_util-fns + +:marked + This cookbook describes how to authenticate against AzureAD and Office365 using Angular + +a#toc +:marked + ## Table of Contents + +a#overview +.l-main-section +:marked + ## Overview \ No newline at end of file