diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 5cd817bb8..048784fb9 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -19,8 +19,6 @@ services: - BasketUrl=http://basket.api:5103 ports: - "5100:5100" - links: - - identity.service:10.0.75.1 webspa: environment: diff --git a/docker-compose.yml b/docker-compose.yml index 030db7df0..bbfd7b671 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -47,3 +47,5 @@ services: basket.data: image: redis + ports: + - "6379:6379" diff --git a/src/Services/Identity/eShopOnContainers.Identity/Configuration/Config.cs b/src/Services/Identity/eShopOnContainers.Identity/Configuration/Config.cs index c8a434215..01a3d0d21 100644 --- a/src/Services/Identity/eShopOnContainers.Identity/Configuration/Config.cs +++ b/src/Services/Identity/eShopOnContainers.Identity/Configuration/Config.cs @@ -41,8 +41,8 @@ namespace eShopOnContainers.Identity.Configuration ClientName = "eShop SPA OpenId Client", AllowedGrantTypes = GrantTypes.Implicit, AllowAccessTokensViaBrowser = true, - RedirectUris = { $"{clientsUrl["Spa"]}/callback.html" }, - PostLogoutRedirectUris = { $"{clientsUrl["Spa"]}/index.html" }, + RedirectUris = { $"{clientsUrl["Spa"]}/" }, + PostLogoutRedirectUris = { $"{clientsUrl["Spa"]}/" }, AllowedCorsOrigins = { $"{clientsUrl["Spa"]}" }, AllowedScopes = { @@ -83,7 +83,8 @@ namespace eShopOnContainers.Identity.Configuration { $"{clientsUrl["Mvc"]}/signin-oidc", "http://104.40.62.65:5100/signin-oidc", - "http://localhost:5100" + "http://localhost:5100/signin-oidc", + "http://13.88.8.119:5100/signin-oidc" }, PostLogoutRedirectUris = new List { diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.html b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.html index 4717b8cbb..55d1d81fe 100644 --- a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.html +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.html @@ -8,7 +8,8 @@
- + +
@@ -39,3 +40,5 @@ + + diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.ts index b32e8929b..08d64256a 100644 --- a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.ts +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/app.component.ts @@ -1,8 +1,10 @@ import { Title } from '@angular/platform-browser'; import { Component, ViewEncapsulation, OnInit } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { Subscription } from 'rxjs/Subscription'; import { DataService } from './shared/services/data.service'; +import { SecurityService } from './shared/services/security.service'; /* * App Component @@ -10,22 +12,24 @@ import { DataService } from './shared/services/data.service'; */ @Component({ - selector: 'appc-app', - styleUrls: ['./app.component.scss'], - templateUrl: './app.component.html' + selector: 'appc-app', + styleUrls: ['./app.component.scss'], + templateUrl: './app.component.html' }) export class AppComponent implements OnInit { + private Authenticated: boolean = false; + subscription: Subscription; + constructor(private titleService: Title, private securityService: SecurityService) { - constructor(private titleService: Title) { + } - } + ngOnInit() { + console.log('app on init'); + this.subscription = this.securityService.authenticationChallenge$.subscribe(res => this.Authenticated = res); + } - ngOnInit() { - - } - - public setTitle(newTitle: string) { - this.titleService.setTitle('eShopOnContainers'); - } + public setTitle(newTitle: string) { + this.titleService.setTitle('eShopOnContainers'); + } } diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/basket/basket-status/basket-status.component.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/basket/basket-status/basket-status.component.ts index 868cd9cc2..06751547b 100644 --- a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/basket/basket-status/basket-status.component.ts +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/basket/basket-status/basket-status.component.ts @@ -19,10 +19,15 @@ export class BasketStatusComponent implements OnInit { ngOnInit() { this.subscription = this.basketEvents.addItemToBasket$.subscribe( item => { + console.log('element received in basket'); console.log(item); - this.service.setBasket(item); - this.service.getBasket().subscribe(basket => { - this.badge = basket.items.length; + this.service.setBasket(item).subscribe(res => { + console.log(res); + this.service.getBasket().subscribe(basket => { + this.badge = basket.items.length; + console.log('response from basket api'); + console.log(basket.items.length); + }); }); }); } diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/basket/basket.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/basket/basket.service.ts index a294f8c87..c8018554e 100644 --- a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/basket/basket.service.ts +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/basket/basket.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@angular/core'; -import { Response } from '@angular/http'; +import { Response, Headers } from '@angular/http'; import { DataService } from '../shared/services/data.service'; +import { SecurityService } from '../shared/services/security.service'; import { IBasket } from '../shared/models/basket.model'; import { IBasketItem } from '../shared/models/basketItem.model'; @@ -19,13 +20,16 @@ export class BasketService { items: [] }; - constructor(private service: DataService) { + constructor(private service: DataService, private authService: SecurityService) { this.basket.items = []; } - setBasket(item) { + setBasket(item): Observable { + console.log('set basket'); this.basket.items.push(item); - this.service.post(this.basket.buyerId, this.basket.items); + return this.service.post(this.basketUrl + '/', this.basket).map((response: Response) => { + return true; + }); } getBasket(): Observable { diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/identity/identity.html b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/identity/identity.html new file mode 100644 index 000000000..f1cfa47b9 --- /dev/null +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/identity/identity.html @@ -0,0 +1 @@ + diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/identity/identity.scss b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/identity/identity.scss new file mode 100644 index 000000000..76ac3a08e --- /dev/null +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/identity/identity.scss @@ -0,0 +1,3 @@ +.identity { + +} diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/identity/identity.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/identity/identity.ts new file mode 100644 index 000000000..1e5fee902 --- /dev/null +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/components/identity/identity.ts @@ -0,0 +1,38 @@ +import { Component, OnInit, OnChanges, Output, Input, EventEmitter } from '@angular/core'; + +import { IIdentity } from '../../models/identity.model'; +import { SecurityService } from '../../services/security.service'; + +@Component({ + selector: 'esh-identity', + templateUrl: './identity.html', + styleUrls: ['./identity.scss'] +}) +export class Identity implements OnInit { + constructor(private service: SecurityService) { + } + + @Output() + changed: EventEmitter = new EventEmitter(); + + @Input() + model: IIdentity; + + ngOnInit() { + console.log("ngOnInit _securityService.AuthorizedCallback"); + + if (window.location.hash) { + this.service.AuthorizedCallback(); + console.log('isAutorized?'); + console.log(this.service.IsAuthorized); + } + } + + login() { + this.service.Authorize(); + } + + logout() { + this.service.Logoff(); + } +} diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/identity.model.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/identity.model.ts new file mode 100644 index 000000000..957dbba36 --- /dev/null +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/models/identity.model.ts @@ -0,0 +1,3 @@ +export interface IIdentity { + +} \ No newline at end of file diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/data.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/data.service.ts index d1e609e9e..20f62a015 100644 --- a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/data.service.ts +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/data.service.ts @@ -8,15 +8,19 @@ import { Observer } from 'rxjs/Observer'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/catch'; +import { SecurityService } from './security.service'; + @Injectable() export class DataService { - - constructor(private http: Http) { } + constructor(private http: Http, private securityService: SecurityService) { } get(url: string, params?: any): Observable { let options: RequestOptionsArgs = {}; - options.headers = new Headers(); - this.addCors(options); + + if (this.securityService) { + options.headers = new Headers(); + options.headers.append("Authorization", "Bearer " + this.securityService.GetToken()); + } return this.http.get(url, options).map( (res: Response) => { @@ -24,13 +28,18 @@ export class DataService { }).catch(this.handleError); } - post(url: string, data: any, params?: any) { - return this.http.post(url, data, params); - } + post(url: string, data: any, params?: any): Observable { + let options: RequestOptionsArgs = {}; + + if (this.securityService) { + options.headers = new Headers(); + options.headers.append("Authorization", "Bearer " + this.securityService.GetToken()); + } - private addCors(options: RequestOptionsArgs): RequestOptionsArgs { - options.headers.append('Access-Control-Allow-Origin', '*'); - return options; + return this.http.post(url, data, options).map( + (res: Response) => { + return res; + }).catch(this.handleError); } private handleError(error: any) { diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/security.service.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/security.service.ts new file mode 100644 index 000000000..8f4776cb6 --- /dev/null +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/services/security.service.ts @@ -0,0 +1,247 @@ +import { Injectable } from '@angular/core'; +import { Http, Response, Headers } from '@angular/http'; +import 'rxjs/add/operator/map'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +//import { Configuration } from '../app.constants'; +import { Router } from '@angular/router'; + +@Injectable() +export class SecurityService { + + private actionUrl: string; + private headers: Headers; + private storage: any; + private authenticationSource = new Subject(); + authenticationChallenge$ = this.authenticationSource.asObservable(); + + constructor(private _http: Http, private _router: Router) { + + //this.actionUrl = _configuration.Server + 'api/DataEventRecords/'; + + this.headers = new Headers(); + this.headers.append('Content-Type', 'application/json'); + this.headers.append('Accept', 'application/json'); + this.storage = sessionStorage; //localStorage; + + if (this.retrieve("IsAuthorized") !== "") { + //this.HasAdminRole = this.retrieve("HasAdminRole"); + this.IsAuthorized = this.retrieve("IsAuthorized"); + } + } + + public IsAuthorized: boolean; + //public HasAdminRole: boolean; + + public GetToken(): any { + return this.retrieve("authorizationData"); + } + + public ResetAuthorizationData() { + this.store("authorizationData", ""); + this.store("authorizationDataIdToken", ""); + + this.IsAuthorized = false; + //this.HasAdminRole = false; + this.store("HasAdminRole", false); + this.store("IsAuthorized", false); + } + + public UserData: any; + public SetAuthorizationData(token: any, id_token:any) { + if (this.retrieve("authorizationData") !== "") { + this.store("authorizationData", ""); + } + + this.store("authorizationData", token); + this.store("authorizationDataIdToken", id_token); + this.IsAuthorized = true; + this.store("IsAuthorized", true); + //emit observable + this.authenticationSource.next(true); + + this.getUserData() + .subscribe(data => this.UserData = data, + error => this.HandleError(error), + () => { + console.log(this.UserData); + }); + } + + public Authorize() { + this.ResetAuthorizationData(); + + console.log("BEGIN Authorize, no auth data"); + + var authorizationUrl = 'http://localhost:5105/connect/authorize'; + var client_id = 'js'; + var redirect_uri = 'http://localhost:5104/'; + var response_type = "id_token token"; + var scope = "openid profile orders basket"; + var nonce = "N" + Math.random() + "" + Date.now(); + var state = Date.now() + "" + Math.random(); + + this.store("authStateControl", state); + this.store("authNonce", nonce); + console.log("AuthorizedController created. adding myautostate: " + this.retrieve("authStateControl")); + + var url = + authorizationUrl + "?" + + "response_type=" + encodeURI(response_type) + "&" + + "client_id=" + encodeURI(client_id) + "&" + + "redirect_uri=" + encodeURI(redirect_uri) + "&" + + "scope=" + encodeURI(scope) + "&" + + "nonce=" + encodeURI(nonce) + "&" + + "state=" + encodeURI(state); + + window.location.href = url; + } + + public AuthorizedCallback() { + console.log("BEGIN AuthorizedCallback, no auth data"); + this.ResetAuthorizationData(); + + var hash = window.location.hash.substr(1); + + var result: any = hash.split('&').reduce(function (result : any, item: string) { + var parts = item.split('='); + result[parts[0]] = parts[1]; + return result; + }, {}); + + console.log(result); + console.log("AuthorizedCallback created, begin token validation"); + + var token = ""; + var id_token = ""; + var authResponseIsValid = false; + if (!result.error) { + + if (result.state !== this.retrieve("authStateControl")) { + console.log("AuthorizedCallback incorrect state"); + } else { + + token = result.access_token; + id_token = result.id_token + + var dataIdToken: any = this.getDataFromToken(id_token); + console.log(dataIdToken); + + // validate nonce + if (dataIdToken.nonce !== this.retrieve("authNonce")) { + console.log("AuthorizedCallback incorrect nonce"); + } else { + this.store("authNonce", ""); + this.store("authStateControl", ""); + + authResponseIsValid = true; + console.log("AuthorizedCallback state and nonce validated, returning access token"); + } + } + } + + if (authResponseIsValid) { + this.SetAuthorizationData(token, id_token); + console.log(this.retrieve("authorizationData")); + + // router navigate to DataEventRecordsList + this._router.navigate(['/dataeventrecords/list']); + } + else { + this.ResetAuthorizationData(); + this._router.navigate(['/Unauthorized']); + } + } + + public Logoff() { + // /connect/endsession?id_token_hint=...&post_logout_redirect_uri=https://myapp.com + console.log("BEGIN Authorize, no auth data"); + + var authorizationUrl = 'http://localhost:5105/connect/endsession'; + console.log(this.retrieve("authorizationDataIdToken")); + var id_token_hint = this.retrieve("authorizationDataIdToken"); + var post_logout_redirect_uri = 'http://localhost:5104/'; + + var url = + authorizationUrl + "?" + + "id_token_hint=" + encodeURI(id_token_hint) + "&" + + "post_logout_redirect_uri=" + encodeURI(post_logout_redirect_uri); + + this.ResetAuthorizationData(); + + window.location.href = url; + } + + public HandleError(error: any) { + console.log(error); + if (error.status == 403) { + this._router.navigate(['/Forbidden']) + } + else if (error.status == 401) { + this.ResetAuthorizationData(); + this._router.navigate(['/Unauthorized']) + } + } + + private urlBase64Decode(str: string) { + var output = str.replace('-', '+').replace('_', '/'); + switch (output.length % 4) { + case 0: + break; + case 2: + output += '=='; + break; + case 3: + output += '='; + break; + default: + throw 'Illegal base64url string!'; + } + + return window.atob(output); + } + + private getDataFromToken(token: any) { + var data = {}; + if (typeof token !== 'undefined') { + var encoded = token.split('.')[1]; + data = JSON.parse(this.urlBase64Decode(encoded)); + } + + return data; + } + + private retrieve(key: string): any { + var item = this.storage.getItem(key); + + if (item && item !== 'undefined') { + return JSON.parse(this.storage.getItem(key)); + } + + return; + } + + private store(key: string, value: any) { + this.storage.setItem(key, JSON.stringify(value)); + } + + private getUserData = (): Observable => { + this.setHeaders(); + return this._http.get('http://localhost:5105/connect/userinfo', { + headers: this.headers, + body: '' + }).map(res => res.json()); + } + + private setHeaders() { + this.headers = new Headers(); + this.headers.append('Content-Type', 'application/json'); + this.headers.append('Accept', 'application/json'); + + var token = this.GetToken(); + + if (token !== "") { + this.headers.append('Authorization', 'Bearer ' + token); + } + } +} \ No newline at end of file diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/shared.module.ts b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/shared.module.ts index b128498fa..c55830018 100644 --- a/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/shared.module.ts +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Client/modules/shared/shared.module.ts @@ -17,9 +17,11 @@ import { DataService } from './services/data.service'; import { UtilityService } from './services/utility.service'; import { UppercasePipe } from './pipes/uppercase.pipe'; import { BasketWrapperService} from './services/basket.wrapper.service'; +import { SecurityService } from './services/security.service'; //Components: import {Pager } from './components/pager/pager'; +import {Identity } from './components/identity/identity'; @NgModule({ imports: [ @@ -39,7 +41,8 @@ import {Pager } from './components/pager/pager'; ErrorSummaryComponent, PageHeadingComponent, UppercasePipe, - Pager + Pager, + Identity ], exports: [ // Modules @@ -57,9 +60,9 @@ import {Pager } from './components/pager/pager'; //HeaderComponent, PageHeadingComponent, UppercasePipe, - Pager + Pager, + Identity ] - }) export class SharedModule { static forRoot(): ModuleWithProviders { @@ -70,7 +73,8 @@ export class SharedModule { DataService, FormControlService, UtilityService, - BasketWrapperService + BasketWrapperService, + SecurityService ] }; } diff --git a/src/Web/WebSPA/eShopOnContainers.WebSPA/Program.cs b/src/Web/WebSPA/eShopOnContainers.WebSPA/Program.cs index 690c56eac..1f25bb104 100644 --- a/src/Web/WebSPA/eShopOnContainers.WebSPA/Program.cs +++ b/src/Web/WebSPA/eShopOnContainers.WebSPA/Program.cs @@ -17,8 +17,8 @@ namespace eShopConContainers.WebSPA .UseKestrel() .UseConfiguration(config) .UseContentRoot(Directory.GetCurrentDirectory()) + .UseUrls("http://localhost:5104/") .UseIISIntegration() - //.UseUrls("http://localhost:5104/") .UseStartup() .Build();