#42 [GSoC] - Implement Open ID login to the App
Closed 5 years ago by a2batic. Opened 6 years ago by ashutoshpw.
ashutoshpw/Fedora-app master  into  master

file modified
+1 -1
@@ -56,11 +56,11 @@ 

      <plugin name="ionic-plugin-keyboard" spec="~2.2.1" />

      <plugin name="cordova-plugin-statusbar" spec="~2.1.3" />

      <plugin name="cordova-plugin-whitelist" spec="~1.2.2" />

-     <plugin name="cordova-plugin-x-socialsharing" spec="^5.1.8" />

      <plugin name="cordova-plugin-calendar" spec="^4.5.5" />

      <plugin name="cordova-plugin-inappbrowser" spec="^1.4.0" />

      <plugin name="cordova-plugin-native-spinner" spec="^1.1.3" />

      <plugin name="cordova-plugin-x-toast" spec="^2.5.2" />

      <engine name="browser" spec="5.0.3" />

      <engine name="android" spec="7.0.0" />

+     <plugin name="cordova-plugin-x-socialsharing" spec="^5.3.2" />

  </widget>

file modified
+5 -3
@@ -36,6 +36,7 @@ 

      "@ionic-native/status-bar": "4.4.0",

      "@ionic-native/toast": "^4.5.3",

      "@ionic/storage": "2.1.3",

+     "angular-oauth2-oidc": "^3.1.4",

      "cordova-android": "7.0.0",

      "cordova-browser": "5.0.3",

      "cordova-plugin-calendar": "^4.5.5",
@@ -48,14 +49,14 @@ 

      "cordova-plugin-splashscreen": "^3.2.2",

      "cordova-plugin-statusbar": "^2.1.3",

      "cordova-plugin-whitelist": "^1.2.2",

-     "cordova-plugin-x-socialsharing": "^5.1.8",

+     "cordova-plugin-x-socialsharing": "^5.3.2",

      "cordova-plugin-x-toast": "^2.5.2",

      "es6-promise-plugin": "^4.2.2",

      "fb": "^1.1.1",

      "ionic-angular": "3.9.2",

      "ionic-plugin-keyboard": "^2.2.1",

      "ionicons": "^3.0.0",

-     "lodash": "^4.14.0",

+     "lodash": "^4.17.5",

      "moment": "^2.14.1",

      "moment-timezone": "^0.5.5",

      "query-string": "^4.2.2",
@@ -66,6 +67,7 @@ 

    },

    "devDependencies": {

      "@ionic/app-scripts": "3.1.8",

+     "@types/lodash": "4.14.105",

      "typescript": "2.4.2"

    },

    "cordova": {
@@ -88,4 +90,4 @@ 

        "android"

      ]

    }

- } 

\ No newline at end of file

+ }

file modified
+54 -11
@@ -1,3 +1,4 @@ 

+ import { OAuthService } from 'angular-oauth2-oidc';

  import {Component, ViewChild} from '@angular/core';

  import { Platform, NavController } from 'ionic-angular';

  
@@ -9,6 +10,9 @@ 

  import {FirstPage} from '../pages/first/first';

  import { SplashScreen } from '@ionic-native/splash-screen';

  

+ import {LoginPage} from '../pages/login/login';

+ import {LogoutPage} from '../pages/logout/logout';

+ 

  @Component({

    templateUrl: 'app.html',

  })
@@ -19,19 +23,58 @@ 

    @ViewChild('content') nav:NavController;

  

  

-   constructor(platform:Platform, splashScreen:SplashScreen) {

-     // used for an example of ngFor and navigation

-     this.pages = [

-       { title: 'Home', component: FirstPage },

-       { title: 'Magazine', component: MagazinePage },

-       { title: 'Social', component: SocialPage },

-       { title: 'Ask', component: AskPage },

-       { title: 'Calendar', component: CalendarPage },

-       { title: 'Women', component: WomenPage }

+   constructor(platform:Platform, splashScreen:SplashScreen, oauthService: OAuthService) {

+ 

+    

+ 

+   

  

-     ];

+     let skipLogin = localStorage.getItem("skipLogin");

+     let oauth_access_token = localStorage.getItem("access_token");

+     if(skipLogin == "1"){

+       this.pages = [

+         { title: 'Home', component: FirstPage },

+         { title: 'Magazine', component: MagazinePage },

+         { title: 'Social', component: SocialPage },

+         { title: 'Ask', component: AskPage },

+         { title: 'Calendar', component: CalendarPage },

+         { title: 'Women', component: WomenPage },

+         { title: 'Login', component: LoginPage }

+       ];

+       this.rootPage = FirstPage;

+     }

+     else{

+       if (oauthService.hasValidIdToken() || oauth_access_token.length > 1) {

+         this.pages = [

+           { title: 'Home', component: FirstPage },

+           { title: 'Magazine', component: MagazinePage },

+           { title: 'Social', component: SocialPage },

+           { title: 'Ask', component: AskPage },

+           { title: 'Calendar', component: CalendarPage },

+           { title: 'Women', component: WomenPage },

+           { title: 'Logout', component: LogoutPage }

+     

+         ];

+         this.rootPage = FirstPage;

+       } else {

+         

+         this.pages = [

+           { title: 'Home', component: FirstPage },

+           { title: 'Magazine', component: MagazinePage },

+           { title: 'Social', component: SocialPage },

+           { title: 'Ask', component: AskPage },

+           { title: 'Calendar', component: CalendarPage },

+           { title: 'Women', component: WomenPage },

+           { title: 'Logout', component: LogoutPage }

+           

+     

+         ];

+         this.rootPage = LoginPage;

Login is strictly optional.

+       }

+       

+     }

  

-     this.rootPage = FirstPage;

+     

  

      platform.ready().then(() => {

        // Okay, so the platform is ready and our plugins are available.

file modified
+11 -2
@@ -1,7 +1,10 @@ 

  import { BrowserModule } from '@angular/platform-browser';

  import { ErrorHandler, NgModule } from '@angular/core';

+ import { OAuthService,UrlHelperService } from 'angular-oauth2-oidc';

  import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';

  import { HttpClientModule } from '@angular/common/http';

+ import { HttpModule } from '@angular/http';

+ 

  import { InAppBrowser } from '@ionic-native/in-app-browser';

  import { SplashScreen } from '@ionic-native/splash-screen';

  import { StatusBar } from '@ionic-native/status-bar';
@@ -20,6 +23,8 @@ 

  import { Browser } from '../providers/browser/browser';

  import { Request } from '../providers/request/request';

  import { IonicConfig } from '../providers/ionic-config/ionic-config';

+ import {LoginPage} from '../pages/login/login';

+ import {LogoutPage} from '../pages/logout/logout';

  

  @NgModule({

    declarations: [
@@ -29,10 +34,11 @@ 

      AskPage,

      CalendarPage,

      SocialPage,

-     WomenPage

+     WomenPage, LoginPage,LogoutPage

    ],

    imports: [

      BrowserModule,

+     HttpModule,

      HttpClientModule,

      IonicModule.forRoot(MyApp)

    ],
@@ -44,7 +50,8 @@ 

      AskPage,

      CalendarPage,

      SocialPage,

-     WomenPage

+     WomenPage, 

+     LoginPage,LogoutPage

    ],

    providers: [

      Browser,
@@ -56,6 +63,8 @@ 

      SpinnerDialog,

      StatusBar,

      Toast,

+     UrlHelperService,

+     OAuthService,

      { provide: ErrorHandler, useClass: IonicErrorHandler }

    ]

  })

file modified
+1 -1
@@ -26,7 +26,7 @@ 

          .catch(err => console.error('Error', err));

      }

    </script>-->

- 

+   <script src="https://ok1static.oktacdn.com/assets/js/sdk/okta-auth-js/1.5.0/OktaAuth.min.js"></script>

    <link href="build/main.css" rel="stylesheet">

  

    <style rel="stylesheet">

@@ -0,0 +1,43 @@ 

+ 

+ <ion-content padding>

+     <ion-title>

+         Log in with FAS

+       </ion-title>

+   <form #loginForm="ngForm" (ngSubmit)="login()" autocomplete="off">

+     <ion-row>

+       <ion-col>

+         <ion-list inset>

+           <ion-item>

+             <ion-input placeholder="Email" name="username" id="loginField" type="text" required [(ngModel)]="username" #email></ion-input>

+           </ion-item>

+           <ion-item>

+             <ion-input placeholder="Password" name="password" id="passwordField" type="password" required [(ngModel)]="password"></ion-input>

+           </ion-item>

+         </ion-list>

+         <a href="https://admin.fedoraproject.org/accounts/user/resetpass" style="padding-left:10px!important;font-size:12px;text-decoration:none;">Forgot Password?</a>

+         

+       </ion-col>

+       

+     </ion-row>

+ 

+     <ion-row class="login" style="margin-bottom:20px!important;">

+       <ion-col>

+         

+         <div *ngIf="error" class="alert alert-danger">{{error}}</div>

+         <button ion-button type="submit" >Log in

+         </button>

+       </ion-col>

+     </ion-row>

+   </form>

+ 

+   <ion-row>

+     <ion-col style="text-align:center;">

+         <p>Don't have a FAS account? <a href="https://admin.fedoraproject.org/accounts/user/new">Sign up now.</a></p>

+       </ion-col>

+   </ion-row>

+   <ion-row>

+       <ion-col style="text-align:center;">

+           <p>Or <a href="#" (click)="skipLogin()">Skip Login.</a></p>

+         </ion-col>

+     </ion-row>

+ </ion-content> 

\ No newline at end of file

@@ -0,0 +1,13 @@ 

+ import { NgModule } from '@angular/core';

+ import { IonicPageModule } from 'ionic-angular';

+ import { LoginPage } from './login';

+ 

+ @NgModule({

+   declarations: [

+     LoginPage,

+   ],

+   imports: [

+     IonicPageModule.forChild(LoginPage),

+   ],

+ })

+ export class LoginPageModule {}

@@ -0,0 +1,33 @@ 

+ page-login {

+     ion-content{

+         margin-top:50px;

+     }

+     ion-title{

+         padding-left:8px;

+     }

+     ion-item {

+         border-radius: 2px !important;

Using !important is bad

+         border: 1px solid #ccc!important;

+         margin-top:10px!important;

+     }

+     ion-list{

+         padding:10px!important;

+         margin:0px!important;

+     }

+     button{

+         background:#3c6eb4!important;

+         padding:20px!important;

+         margin:0px auto!important;

+         float:left!important;

+     }

+     a{

+         color: #3c64b4!important;

+         text-decoration: none;

+     }

+     p{

+         color:#444;

+     }

+     ion-row.login{

+         padding:0px 10px!important;

+     }

+ }

@@ -0,0 +1,71 @@ 

+ import { Component, ViewChild } from '@angular/core';

+ import { NavController } from 'ionic-angular';

+ import { OAuthService } from 'angular-oauth2-oidc';

+ import { FirstPage } from '../../pages/first/first';

+ declare const OktaAuth: any;

+ 

+ @Component({

+   selector: 'page-login',

+   templateUrl: 'login.html'

+ })

+ export class LoginPage {

+   @ViewChild('email') email: any;

+   private username: string;

+   private password: string;

+   private error: string;

+ 

+   constructor(private navCtrl: NavController, private oauthService: OAuthService) {

+     oauthService.redirectUri = window.location.origin;

+     oauthService.clientId = '0oavpasz2xMrGVsjz2p6';

+     oauthService.scope = 'openid profile email';

+     oauthService.oidc = true;

+     oauthService.issuer = 'https://w3dev.okta.com';

+   }

+ 

+   ionViewDidLoad(): void {

+     setTimeout(() => {

+       this.email.setFocus();

I think this should be doable by autofocus

+     }, 500);

+   }

+  

+   skipLogin(){

+     localStorage.setItem("skipLogin","1");

+     this.navCtrl.setRoot(FirstPage);

+   }

+   login(): void {

+     localStorage.setItem("skipLogin","0");

+     this.oauthService.createAndSaveNonce().then(nonce => {

+       const authClient = new OktaAuth({

Why Otka? The aim is to add integration with id.fp.o.

+         clientId: this.oauthService.clientId,

+         redirectUri: this.oauthService.redirectUri,

+         url: this.oauthService.issuer

+       });

+       authClient.signIn({

+         username: this.username,

+         password: this.password

+       }).then((response) => {

+         if (response.status === 'SUCCESS') {

+           authClient.token.getWithoutPrompt({

+             nonce: nonce,

+             responseType: ['id_token', 'token'],

+             sessionToken: response.sessionToken,

+             scopes: this.oauthService.scope.split(' ')

+           })

+             .then((tokens) => {

+               // oauthService.processIdToken doesn't set an access token

+               // set it manually so oauthService.authorizationHeader() works

+               localStorage.setItem('access_token', tokens[1].accessToken);

+               this.oauthService.processIdToken(tokens[0].idToken, tokens[1].accessToken);

+               this.navCtrl.setRoot(FirstPage);

+             })

+             .catch(error => console.error(error));

+         } else {

+           throw new Error('We cannot handle the ' + response.status + ' status');

+         }

+       }).fail((error) => {

+         console.error(error);

+         this.error = error.message;

+       });

+     });

+   }

+ } 

\ No newline at end of file

@@ -0,0 +1,18 @@ 

+ <!--

+   Generated template for the LogoutPage page.

+ 

+   See http://ionicframework.com/docs/components/#navigation for more info on

+   Ionic pages and navigation.

+ -->

+ <ion-header>

+ 

+   <ion-navbar>

+     <ion-title>logout</ion-title>

+   </ion-navbar>

+ 

+ </ion-header>

+ 

+ 

+ <ion-content padding>

+ 

+ </ion-content>

@@ -0,0 +1,13 @@ 

+ import { NgModule } from '@angular/core';

+ import { IonicPageModule } from 'ionic-angular';

+ import { LogoutPage } from './logout';

+ 

+ @NgModule({

+   declarations: [

+     LogoutPage,

+   ],

+   imports: [

+     IonicPageModule.forChild(LogoutPage),

+   ],

+ })

+ export class LogoutPageModule {}

@@ -0,0 +1,3 @@ 

+ page-logout {

+ 

+ }

@@ -0,0 +1,28 @@ 

+ import { Component } from '@angular/core';

+ import { IonicPage, NavController, NavParams } from 'ionic-angular';

+ import { LoginPage } from '../login/login';

+ 

+ /**

+  * Generated class for the LogoutPage page.

+  *

+  * See https://ionicframework.com/docs/components/#navigation for more info on

+  * Ionic pages and navigation.

+  */

+ 

+ @IonicPage()

+ @Component({

+   selector: 'page-logout',

+   templateUrl: 'logout.html',

+ })

+ export class LogoutPage {

+ 

+   constructor(public navCtrl: NavController, public navParams: NavParams) {

+     localStorage.setItem("access_token","0");

+     this.navCtrl.setRoot(LoginPage);

+   }

+ 

+   ionViewDidLoad() {

+     console.log('ionViewDidLoad LogoutPage');

+   }

+ 

+ }

file modified
+17 -3
@@ -20,18 +20,22 @@ 

    // TODO: Refactor, we are not supposed to use `any`, rather, ww should define an

    // interface Post { content:string, date:Date }

    private posts:Array<any>;

+   private tagged_updates:Array<any>;

    private tweets:Array<any>;

    private updates:Array<any>;

    private USER:any;

- 

+   

    constructor(private browser:Browser, private fb:FB, private tw:Tw,

                private socialSharing:SocialSharing) {

      this.posts = [];

+     this.tagged_updates = [];

      this.tweets = [];

      this.updates = [];

+     

  

      this.USER = {

        FB: 'fedoraqa',

+       FB_TAGGED: ['TheFedoraProject'],

        TW: 'fedora_qa',

      };

    }
@@ -46,7 +50,17 @@ 

        .then((posts:Array<any>) => {

          this.posts = posts;

          this.mergeUpdates();

-       }).catch((err) => console.log(err + "kanika"));

+       }).catch((err) => console.log(err));

+ 

+ 

+       this.fb

+       .getTaggedPosts(this.USER.FB_TAGGED[0])

+       .then((posts:Array<any>) => {

+         this.tagged_updates = posts;

+         this.mergeUpdates();

+       }).catch((err) => console.error(err));

+     

+     

  

      this.tw

        .getTimelineTweets(this.USER.TW)
@@ -58,7 +72,7 @@ 

  

    mergeUpdates() {

      // TODO: Merge as per ascending order of timestamps?

-     this.updates = [ ...this.posts, ...this.tweets ];

+     this.updates = [ ...this.posts, ...this.tagged_updates, ...this.tweets ];

    }

  

    openUpdate(event) {

file modified
+26
@@ -10,6 +10,7 @@ 

    version: 'v2.6'

  };

  

+ 

  /*

    Generated class for the Fb provider.

  
@@ -19,6 +20,7 @@ 

  @Injectable()

  export class FB {

    private fb:Facebook;

+   

    constructor() {

      this.fb = new Facebook(FB_CONFIG);

    }
@@ -34,6 +36,7 @@ 

        });

      });

    }

+   

  

    getPagePosts(page) {

      return new Promise((resolve, reject) => {
@@ -57,4 +60,27 @@ 

        }).catch(reject);

      });

    }

+ 

+   getTaggedPosts(page) {

+     return new Promise((resolve, reject) => {

+       this.api([page, 'tagged']).then((res:any) => {

+         var posts = _.compact(_.map(res.data, p => {

+           var post = {

+             id: p.id,

+             link: 'https://facebook.com/' + p.id,

+             content: p.message,

+             origin: 'facebook',

+           };

+ 

+           if (_.isEmpty(post.content)) {

+             return null;

+           } else {

+             return post;

+           }

+         }));

+ 

+         return resolve(posts);

+       }).catch(reject);

+     });

+   }

  }

I have implemented OpenID login to the app (as I figured out the FAS login is based on OpenID) so admin can change the credentials and it should work for FAS login as well.

For testing

Email:
fedora@w3dev.in

Password:
Password@1233

Note - the Current system is linked with Okta Login

Login is strictly optional.

I think this should be doable by autofocus

Why Otka? The aim is to add integration with id.fp.o.

@amitosh
Login is strictly optional and hence there is an option to Skip Login on the flash screen.

  • Will Implement autofocus
  • Implemented via Okta as I had to implement any OpenID login as I was aware of the fact that FAS works on OpenID.
  • WIll implement Fedora FAS with your provided links.

I will complete this by this weekend.

Pull-Request has been closed by a2batic

5 years ago