DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • A Developer's Guide to Database Sharding With MongoDB
  • Kafka Link: Ingesting Data From MongoDB to Capella Columnar
  • Java and MongoDB Integration: A CRUD Tutorial [Video Tutorial]
  • Angular Component Tree With Tables in the Leaves and a Flexible JPA Criteria Backend

Trending

  • The Human Side of Logs: What Unstructured Data Is Trying to Tell You
  • Four Essential Tips for Building a Robust REST API in Java
  • Mastering Advanced Traffic Management in Multi-Cloud Kubernetes: Scaling With Multiple Istio Ingress Gateways
  • Navigating the LLM Landscape: A Comparative Analysis of Leading Large Language Models
  1. DZone
  2. Data Engineering
  3. Databases
  4. An Angular PWA From Front-End to Backend: Creating a Login Process

An Angular PWA From Front-End to Backend: Creating a Login Process

Learn how to make an Angular PWA that uses Spring Boot on the backend that can handle login processes.

By 
Sven Loesekann user avatar
Sven Loesekann
·
May. 08, 19 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
12.1K Views

Join the DZone community and get the full member experience.

Join For Free

This is the second part of the series about the AngularPwaMessenger project. It is a chat system with offline capability. The first part showed how to to signin as a user and add users to your contacts. The users where stored on the server and the user and its contacts where stored in the indexed DB.

This part will be about the Login online and offline. The Login is discussed as a process on the client- and server-side.

The encryption of the messages will not be discusses because it adds optional complexity.

The User Record

To enable logging in online and offline, the user record has to be stored on the server and on the client. On the server. it is stored in MongoDB and on the client it is stored in the browser's indexed DB.

On the server. the user record can be found in MsgUser:

@Id
private ObjectId _id;
@JsonProperty
private Date createdAt = new Date();
@Indexed( unique=true)
@JsonProperty
private String username;
@JsonProperty
private String password;
@Indexed( unique=true)
@JsonProperty
private String email;
@JsonProperty
private String token;
@JsonProperty
private String base64Avatar;
@JsonProperty
private String publicKey;
@JsonProperty
private String privateKey;
@JsonProperty
private String userId;
@JsonProperty
private String salt;
@JsonProperty
private boolean confirmed = false;
@JsonProperty
private String uuid;

The _id is the MongoDB id that gets generated. The username is the unique name of the user/contact. The password is a hashed value for the login. The email is a unique value that can be used for the confirm email feature. The token is the JWT token that is sent to the client for authentification. It gets filed after each successful login. The user can store a small icon as an avatar in the base64Avatar field. The publicKey/privateKey properties store the keys for asymetric encryption. The privateKey is stored in a wrapped format. The userId is the property where the MongoDB key is sent to the browser. The salt is used for the encryption of the indexed DB records. The properties confirmed and uuid can be used for the 'confirm email' feature.

On the browser, the user's records can be found in LocalUser:

export interface LocalUser {
  id?: number;
  createdAt: Date;
  username: string;
  hash: string;
  salt: string;
  email: string;
  base64Avatar: string;
  publicKey: string;
  privateKey: string;
  userId: string;
}

The id is an auto-incrementing number. The username is unique due to contraints on the server. The hash is the hashed password. The salt is used for local encryption. The base64Avatar can store an icon for the user. The publicKey/privateKey properties are used to encrypt/decrypt the messages. The privateKey is stored in a wrapped format. The userId is the MongoDB ID.

The server stores the user's records so that it can be recreated if it is lost in the browser. The local user's records can be used to login if the client is offline and has the values to decrypt the messages in the index DB. Those are the user records the login is based on.

The Start of the Login Process

At the beginning of the login process, the app has to decide if an online or offline login is needed.

The login starts with the onLoginClick() method in the login.component.ts:

onLoginClick(): void {
    let myUser = new MyUser();
    myUser.username = this.loginForm.get( 'username' ).value;
    myUser.password = this.loginForm.get( 'password' ).value;      
    if ( this.connected ) {
      this.cryptoService.hashServerPW( this.loginForm.get( 'password' ).value ).then( value => {
        myUser.password = value;
        this.authenticationService.postLogin( myUser ).subscribe( us => {
          let myLocalUser: LocalUser = {
            base64Avatar: null,
            createdAt: null,
            email: null,
            hash: null,
            publicKey: null,
            privateKey: null,
            salt: null,
            username: us.username,
            userId: null
          };
          this.localdbService.loadUser( myLocalUser )
            .then( localUserList => localUserList.toArray() )
            .then( localUserArray => {
              if ( localUserArray.length > 0 ) {
                us.password = this.loginForm.get( 'password' ).value;
                this.login( us, localUserArray[0] );
              } else {
                this.createLocalUser( us , this.loginForm.get( 'password' ).value).then( result => {
                  us.password = this.loginForm.get( 'password' ).value;
                  this.login( us, result );
                } );
              }
            } );
        }, err => {
          myUser.password = this.loginForm.get( 'password' ).value;
          this.localLogin( myUser ); 
          });
      } );
    } else {
      this.localLogin( myUser );
    }
  }

In line 2, the MyUser class is created. It is the dto that is posted to the server.

In lines 3-4, the username and the password are set in the MyUser class.

In line 5, the browser checks if it has a network connection.

In lines 6-7, the password is replaced in MyUser with the server hash.

In line 8, we post MyUser to the server and, if the server responds, the code continues.

In lines 9-19, the LocalUser interface is created with the username.

In lines 20-22, the LocaldbService is called to load the user. It returns an array that has 0 or 1 user(s).

In lines 23-31, we check to see if the user was found. If yes, the MyUser password is set back to the user password and the login method is called. If no, the createLocalUser method is called to recreate the local user record and then the password is set back and the login is called.

In lines 33-36, the case of a server error is handled. The localLogin method is called to use the offline mode because the server is unavailable.

In line 39, the case that the browser knows it is offline is handled. The localLogin method is called to use the offline mode.

The Login Process on the Server-Side

The server-side login process can be found in the AuthenticationController.java file of my GitHub repo:

@PostMapping("/login")
public Mono<MsgUser> postUserLogin(@RequestBody MsgUser myUser) {
  Query query = new Query();
  query.addCriteria(Criteria.where("username").is(myUser.getUsername()));
  if (this.confirmUrl != null && !this.confirmUrl.isBlank()) {
    query.addCriteria(Criteria.where("confirmed").is(true));
  }
  return this.operations.findOne(query, MsgUser.class).switchIfEmpty(Mono.just(new MsgUser()))
    .map(user1 -> {
      if(user1.get_id() != null) user1.setUserId(user1.get_id().toString());
      return user1;
    }).map(user1 -> loginHelp(user1, myUser.getPassword())).onErrorResume(re -> { 
      LOG.info("login failed for: "+myUser.getUsername(),re);
      return Mono.just(new MsgUser());
    });
}

private MsgUser loginHelp(MsgUser user, String passwd) {
  if (user.getUsername() != null) {
    if (this.passwordEncoder.matches(passwd, user.getPassword())) {
      String jwtToken = this.jwtTokenProvider.createToken(user.getUsername(), 
        Arrays.asList(Role.USERS), Optional.empty());
      user.setToken(jwtToken);
      user.setPassword("XXX");
      return user;
    }
  }
  return new MsgUser();
}

In lines 1-2, the REST endpoint gets created with the annotation and the mapping of the request body in the dto.

In lines 3-4, the MongoDB query is built. It searches for a matching username.

In lines 5-7, the the confirm email feature is handled.

In line 8, the query is sent to MongoDB and if the user is not found an empty dto is returned.

In lines 9-11, the MongoDB ID is mapped in the UserId property.

In lines 12-15, the loginHelp method is called with the supplied password and onErrorResume returns an empty dto if it gets called.

In line 19, the username is checked if it is found the user exists.

In line 20, the password is checked against he hash in MongoDB.

In lines 21-25, the JWT Token is created. It is set in the dto and dto and the password hash is removed in the dto.

The Login Without the Local User

Without the local user in the indexed DB the user record is recreated of the server dto. That is done in the createLocalUser method.

private createLocalUser( us: MyUser, passwd: string): PromiseLike<LocalUser> {
    let localUser: LocalUser = null;
    return this.cryptoService.generateKey( passwd, us.salt ? us.salt : null )
      .then( ( result ) => {
        localUser = {
          base64Avatar: us.base64Avatar,
          createdAt: us.createdAt,
          email: us.email,
          hash: result.a,
          salt: result.b,
          username: us.username,
          publicKey: us.publicKey,
          privateKey: us.privateKey,
          userId: us.userId
        };
        return localUser;
      } ).then( myLocalUser => this.localdbService.storeUser( myLocalUser ) )
      .then( value => Promise.resolve( value ) ).then( () => localUser );
  }

In line 1, the method is created with the MyUser dto that comes from the server and the password the user has provided.

In line 3, generateKey is called to recreate the local key based of the salt in the MyUser dto from the server.

In lines 4-16, the localUser dto is populated.

In line 17, the localUser is stored in the indexed DB with the localdbService.

In line 18, Promise.resolve(...) is called to wait until the localUser is persisted and then a Promise with the localUser is returned.

The Local Login

In case of a missing network or a missing server response localLogin is called:

  private localLogin( myUser: MyUser ) {
    let myLocalUser: LocalUser = {
      base64Avatar: null,
      createdAt: null,
      email: null,
      hash: null,
      publicKey: null,
      privateKey: null,
      salt: null,
      username: myUser.username,
      userId: null
    };
    this.localdbService.loadUser( myLocalUser ).then( localUserList =>
      localUserList.first().then( myLocalUser => {
        myUser.userId = myLocalUser.userId;
        return this.login( myUser, myLocalUser ); 
        }) );
  }

In lines 2-12, the LocalUser dto is created with the unique username that the user provided.

In lines 13-14, the LocaldbService loads the the user record from the indexed DB.

In lines 15-16, the userId is set in the myUser dto and the login method is called.

The user record in the indexed DB has the salt and the hash to do the login but without the JWT token it is impossible to query the server.

Finally, the Login

The login method is called for the local and the remote login.

 login( us: MyUser, localUser: LocalUser ): void {
    this.cryptoService.generateKey( us.password, localUser.salt ).then( tuple => {
      if ( ( us.username !== null || localUser.username !== null ) && localUser.hash === tuple.a ) {
        if ( us.token ) {
          this.jwttokenService.jwtToken = us.token;
        }        
        this.loginFailed = false;
        this.cryptoService.hashPW( us.password ).then( value => {
          us.password = value;
          us.salt = localUser.salt;
          this.data.myUser = us;
          this.dialogRef.close( this.data.myUser );
        } );
      } else {
        this.loginFailed = true;
      }
    } );
  }

In line 1, the login method is created and the MyUser parameter has the username and the user provided password. The LocalUser parameter has the indexed DB user record.

In line 2, the the hash for the of the password is created. It needs the password and the salt of the LocalUser record.

In line 3, the it is checked that the local username and the provided username exist and the created hash is checked against the hash in LocalUser record. If these checks are successful the user is logged in.

In lines 4-6, it is checked if the server has provided a JWT token. That means it is a successful online login. Then the token is set in the JwtTokenService. Without this token, it is not possible to get contacts or messages from the server.

In line 7, the UI flag for a successful login is is set.

In lines 8-13, the password is hashed by the hashPW function and set in the MyUser dto. The salt is set in the MyUser dto and the login dialog is closed.

In line 15, the UI flag is set to show the login failed message in the login dialog.

Conclusion

This is the login process of the AngularPwaMessenger. To make the process work locally, the salt and the password hash need to be stored in the indexed DB and that user record is used. The server provides the JWT token for authentication of the REST calls to the server. If the JWT token is unavailable, the PWA is offline and the local login with the indexed DB is used. The password is neither stored or sent somewhere. To save the user from losing the keys the public/private (wrapped) keys are stored on the server; the salt for the indexed DB is stored too. That makes it possible to recreate the local user record from the server.

This makes it possible to provide a login for an Angular PWA. With the help of Dexie and Angular, this amount of code is sufficiant for the login process. In the next part of the series, the sending and receiving of messages will be discussed.

Picasa Web Albums Database AngularJS MongoDB

Opinions expressed by DZone contributors are their own.

Related

  • A Developer's Guide to Database Sharding With MongoDB
  • Kafka Link: Ingesting Data From MongoDB to Capella Columnar
  • Java and MongoDB Integration: A CRUD Tutorial [Video Tutorial]
  • Angular Component Tree With Tables in the Leaves and a Flexible JPA Criteria Backend

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!