6

My stack is:

  • Ionic 2
  • Java Spring
  • JWT authentication

and I want to implement a Social login button (Facebook, Google etc.) inside my app with the respective cordova plugins, that logs in the user and validates him on my existing custom server-side API and store his/her data. I wasn't able to find any good tutorial on how to do this.

I want my user to be persisted in the database with a random password and be able to login from my app.

I was imagining something along the lines of:

(client-side)

FB.login(function(userDetailsAndToken) {
    myBackendAPI.socialLogin(userDetailsAndToken).then(function(user) {
        //Successfully logged in with Facebook!
        this.user = user;
    }
}

and in the backend (Java Spring):

@PostMapping("/social/account")
public ResponseEntity socialAccount(@Valid @RequestBody FacebookDTO facebookDTO) {
    validateSocialUser(facebookDTO);

    //if user exists return invalid etc.
    Optional<User> existingUser = userRepository.findOneByEmail(facebookDTO.getEmail());
    if (existingUser.isPresent()) {
        return ResponseEntity.badRequest();
    }

    //User doesn't exist. Proceed with login 
    //map from social to my User entity
    User user = socialMapper.toEntity(facebookDTO);

    userService
        .createUser(user.getLogin(), user.getPassword(),
            user.getFirstName(), user.getLastName(),
            user.getEmail().toLowerCase(), user.getImageUrl(),
            user.getLangKey());
    return new ResponseEntity<>(HttpStatus.CREATED);
}

Is this possible and secure? Any good resources/libraries or guidelines on how to achieve that?

Alex Arvanitidis
  • 4,403
  • 6
  • 26
  • 36

3 Answers3

4

Here is a sample demo about working with FB and Google Auth , but i'm not from a Java background so you will be finding only client side solution.

service

let’s implement the logic for the login functionality. Create an oauth.service.ts file in the oauth folder and paste the following code in there:

import { Injectable, Injector } from '@angular/core';
import { FacebookOauthProvider } from './facebook/facebook-oauth.provider';
import { IOathProvider } from './oauth.provider.interface';
import { GoogleOauthProvider } from './google/google-oauth.provider';
import { OAuthToken } from './models/oauth-token.model';
@Injectable()
export class OAuthService {
    private oauthTokenKey = 'oauthToken';
    private injector: Injector;
constructor(injector: Injector) {
        this.injector = injector;
    }
login(source: string): Promise {
        return this.getOAuthService(source).login().then(accessToken => {
            if (!accessToken) {
                return Promise.reject('No access token found');
            }
let oauthToken = {
                accessToken: accessToken,
                source: source
            };
            this.setOAuthToken(oauthToken);
            return oauthToken;
        });
    }
getOAuthService(source?: string): IOathProvider {
        source = source || this.getOAuthToken().source;
        switch (source) {
            case 'facebook':
                return this.injector.get(FacebookOauthProvider);
            case 'google':
                return this.injector.get(GoogleOauthProvider);
            default:
                throw new Error(`Source '${source}' is not valid`);
        }
    }
setOAuthToken(token: OAuthToken) {
        localStorage.setItem(this.oauthTokenKey, JSON.stringify(token));
    }
getOAuthToken(): OAuthToken {
        let token = localStorage.getItem(this.oauthTokenKey);
        return token ? JSON.parse(token) : null;
    }
}

Authentication provider and token interfaces As we have already mentioned, the IOathProvider should include a login() function. Therefore, we should set the following interface that will act as an abstract type/model for the IOathProvider object. Create an oauth.provider.interface.ts file in the oauth folder and include the following lines in it:

export interface IOathProvider {
    login(): Promise;
}

Facebook and Google authentication services

As a next step, we should implement the services for each one of the authentication providers our app has, i.e. FacebookOauthProvider and GoogleOauthProvider.

Install dependencies

Here is when the ng2-cordova-oauth library comes handy. We can install it by executing the command: npm install ng2-cordova-oauth --save

Also, our app relies on the Cordova InAppBrowser plugin. We are going to install it with:

ionic plugin add cordova-plugin-inappbrowser

Do not forget to include cordova-plugin-inappbrowser in your package.json file too, so it can be installed with the rest of the plugins anytime you install your project from scratch.

Implement the Facebook and Google authentication providers

Let’s create the facebook-oauth.provider.ts file under the oauth/facebook/ path. In this file, include the code in the snippet:

 import { Injectable } from '@angular/core';
    import { Http } from '@angular/http';
    import { IOathProvider } from '../oauth.provider.interface';
    import { CordovaOauth } from 'ng2-cordova-oauth/oauth';
    import { Facebook } from 'ng2-cordova-oauth/provider/facebook';
    import { Config } from '../../../config';
    interface ILoginResponse {
        access_token: string;
    }
    @Injectable()
    export class FacebookOauthProvider implements IOathProvider {
        private cordovaOauth: CordovaOauth;
        private http: Http;
        private config: Config;
        private facebook: Facebook;
    constructor(http: Http, config: Config) {
            this.http = http;
            this.config = config;
            this.facebook = new Facebook({ clientId: config.facebook.appId, appScope: config.facebook.scope });
            this.cordovaOauth = new CordovaOauth();
        }
    login(): Promise {
            return this.cordovaOauth.login(this.facebook)
                .then((x: ILoginResponse) => x.access_token);
        }
    }

Similarly, using the CordovaOauth object available by the ng2-cordova-oauth library, we will implement the Google authentication provider with its own login() function. However, here we pass another clientId from Config that corresponds to the application we configured with Google using the Google Developer Console. Therefore, create a google-oauth.provider.ts file and paste the following lines:

import { Injectable } from '@angular/core';
import { IOathProvider } from '../oauth.provider.interface';
import { OAuthProfile } from '../models/oauth-profile.model';
import { CordovaOauth } from 'ng2-cordova-oauth/oauth';
import { Google } from 'ng2-cordova-oauth/provider/google';
import { Config } from '../../../config';
import { Http } from '@angular/http';
interface ILoginResponse {
    access_token: string;
}
@Injectable()
export class GoogleOauthProvider implements IOathProvider {
    private http: Http;
    private config: Config;
    private cordovaOauth: CordovaOauth;
    private google: Google;
constructor(http: Http, config: Config) {
        this.http = http;
        this.config = config;
        this.google = new Google({ clientId: config.google.appId, appScope: config.google.scope });
        this.cordovaOauth = new CordovaOauth();
    }
login(): Promise {
        return this.cordovaOauth.login(this.google).then((x: ILoginResponse) => x.access_token);
    }
getProfile(accessToken: string): Promise {
        let query = `access_token=${accessToken}`;
        let url = `${this.config.google.apiUrl}userinfo?${query}`;
return this.http.get(url)
            .map(x => x.json())
            .map(x => {
                let name = x.name.split(' ');
                return {
                    firstName: name[0],
                    lastName: name[1],
                    email: x.email,
                    provider: 'google'
                };
            })
            .toPromise();
    }
}

Full credits to this Article and you can find the Working Code in Github. I haven't covered the whole Tutorial , only the parts (Google and Facebook) of that tutorial .i.e. What plugin we need to install and how to use using the TypeScript , if you need beyond that then you can refer to that tutorial

Krsna Kishore
  • 8,233
  • 4
  • 32
  • 48
  • Although the most important part for me is the Java part, this is quite an extensive response and thank you for it! Looks awesome! I'm currently stuck in the Java part cause Spring Social seems to be quite hard to grasp as a REST API. – Alex Arvanitidis Aug 29 '17 at 13:55
  • @AlexArvanitidis , i may not no the Java part but , for each unique user it will create a unique id which you will be obtanined after registering so as it is a REST Api you can retrieve the Unique `ID` and based on that you can develop your custom logic – Krsna Kishore Aug 29 '17 at 16:12
2

Examples from one of my projects. Java Client for JWT Authentication:

import org.springframework.cloud.client.loadbalancer.RestTemplateCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.web.client.RestTemplate;

import java.util.Map;

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends ResourceServerConfigurerAdapter {

    public MicroserviceSecurityConfiguration() {
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
            .disable()
            .headers()
            .frameOptions()
            .disable()
        .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeRequests()
            .antMatchers("/api/profile-info").permitAll()
            .antMatchers("/api/**").authenticated()
            .antMatchers("/management/health").permitAll()
            .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
            .antMatchers("/swagger-resources/configuration/ui").permitAll();
    }

    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(
            @Qualifier("loadBalancedRestTemplate") RestTemplate keyUriRestTemplate) {

        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(getKeyFromAuthorizationServer(keyUriRestTemplate));
        return converter;
    }

    @Bean
    public RestTemplate loadBalancedRestTemplate(RestTemplateCustomizer customizer) {
        RestTemplate restTemplate = new RestTemplate();
        customizer.customize(restTemplate);
        return restTemplate;
    }

    private String getKeyFromAuthorizationServer(RestTemplate keyUriRestTemplate) {
        HttpEntity<Void> request = new HttpEntity<Void>(new HttpHeaders());
        return (String) keyUriRestTemplate
            .exchange("http://someserver/oauth/token_key", HttpMethod.GET, request, Map.class).getBody()
            .get("value");

    }
}

Java backend for social buttons including (Google and Facebook):

import package.repository.SocialUserConnectionRepository;
import package.repository.CustomSocialUsersConnectionRepository;
import package.security.social.CustomSignInAdapter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.social.UserIdSource;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurer;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.ConnectionRepository;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.web.ConnectController;
import org.springframework.social.connect.web.ProviderSignInController;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.social.connect.web.SignInAdapter;
import org.springframework.social.facebook.connect.FacebookConnectionFactory;
import org.springframework.social.google.connect.GoogleConnectionFactory;
import org.springframework.social.security.AuthenticationNameUserIdSource;
import org.springframework.social.twitter.connect.TwitterConnectionFactory;
// jhipster-needle-add-social-connection-factory-import-package

import javax.inject.Inject;

/**
 * Basic Spring Social configuration.
 *
 * <p>Creates the beans necessary to manage Connections to social services and
 * link accounts from those services to internal Users.</p>
 */
@Configuration
@EnableSocial
public class SocialConfiguration implements SocialConfigurer {
    private final Logger log = LoggerFactory.getLogger(SocialConfiguration.class);

    @Inject
    private SocialUserConnectionRepository socialUserConnectionRepository;

    @Inject
    Environment environment;

    @Bean
    public ConnectController connectController(ConnectionFactoryLocator connectionFactoryLocator,
            ConnectionRepository connectionRepository) {

        ConnectController controller = new ConnectController(connectionFactoryLocator, connectionRepository);
        controller.setApplicationUrl(environment.getProperty("spring.application.url"));
        return controller;
    }

    @Override
    public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
        // Google configuration
        String googleClientId = environment.getProperty("spring.social.google.clientId");
        String googleClientSecret = environment.getProperty("spring.social.google.clientSecret");
        if (googleClientId != null && googleClientSecret != null) {
            log.debug("Configuring GoogleConnectionFactory");
            connectionFactoryConfigurer.addConnectionFactory(
                new GoogleConnectionFactory(
                    googleClientId,
                    googleClientSecret
                )
            );
        } else {
            log.error("Cannot configure GoogleConnectionFactory id or secret null");
        }

        // Facebook configuration
        String facebookClientId = environment.getProperty("spring.social.facebook.clientId");
        String facebookClientSecret = environment.getProperty("spring.social.facebook.clientSecret");
        if (facebookClientId != null && facebookClientSecret != null) {
            log.debug("Configuring FacebookConnectionFactory");
            connectionFactoryConfigurer.addConnectionFactory(
                new FacebookConnectionFactory(
                    facebookClientId,
                    facebookClientSecret
                )
            );
        } else {
            log.error("Cannot configure FacebookConnectionFactory id or secret null");
        }

        // Twitter configuration
        String twitterClientId = environment.getProperty("spring.social.twitter.clientId");
        String twitterClientSecret = environment.getProperty("spring.social.twitter.clientSecret");
        if (twitterClientId != null && twitterClientSecret != null) {
            log.debug("Configuring TwitterConnectionFactory");
            connectionFactoryConfigurer.addConnectionFactory(
                new TwitterConnectionFactory(
                    twitterClientId,
                    twitterClientSecret
                )
            );
        } else {
            log.error("Cannot configure TwitterConnectionFactory id or secret null");
        }

        // jhipster-needle-add-social-connection-factory
    }

    @Override
    public UserIdSource getUserIdSource() {
        return new AuthenticationNameUserIdSource();
    }

    @Override
    public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
        return new CustomSocialUsersConnectionRepository(socialUserConnectionRepository, connectionFactoryLocator);
    }

    @Bean
    public SignInAdapter signInAdapter() {
        return new CustomSignInAdapter();
    }

    @Bean
    public ProviderSignInController providerSignInController(ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository usersConnectionRepository, SignInAdapter signInAdapter) throws Exception {
        ProviderSignInController providerSignInController = new ProviderSignInController(connectionFactoryLocator, usersConnectionRepository, signInAdapter);
        providerSignInController.setSignUpUrl("/social/signup");
        providerSignInController.setApplicationUrl(environment.getProperty("spring.application.url"));
        return providerSignInController;
    }

    @Bean
    public ProviderSignInUtils getProviderSignInUtils(ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository usersConnectionRepository) {
        return new ProviderSignInUtils(connectionFactoryLocator, usersConnectionRepository);
    }
}

package package.repository;

import package.domain.SocialUserConnection;

import org.springframework.social.connect.*;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class CustomSocialUsersConnectionRepository implements UsersConnectionRepository {

    private SocialUserConnectionRepository socialUserConnectionRepository;

    private ConnectionFactoryLocator connectionFactoryLocator;

    public CustomSocialUsersConnectionRepository(SocialUserConnectionRepository socialUserConnectionRepository, ConnectionFactoryLocator connectionFactoryLocator) {
        this.socialUserConnectionRepository = socialUserConnectionRepository;
        this.connectionFactoryLocator = connectionFactoryLocator;
    }

    @Override
    public List<String> findUserIdsWithConnection(Connection<?> connection) {
        ConnectionKey key = connection.getKey();
        List<SocialUserConnection> socialUserConnections =
            socialUserConnectionRepository.findAllByProviderIdAndProviderUserId(key.getProviderId(), key.getProviderUserId());
        return socialUserConnections.stream()
            .map(SocialUserConnection::getUserId)
            .collect(Collectors.toList());
    };

    @Override
    public Set<String> findUserIdsConnectedTo(String providerId, Set<String> providerUserIds) {
        List<SocialUserConnection> socialUserConnections =
            socialUserConnectionRepository.findAllByProviderIdAndProviderUserIdIn(providerId, providerUserIds);
        return socialUserConnections.stream()
            .map(SocialUserConnection::getUserId)
            .collect(Collectors.toSet());
    };

    @Override
    public ConnectionRepository createConnectionRepository(String userId) {
        if (userId == null) {
            throw new IllegalArgumentException("userId cannot be null");
        }
        return new CustomSocialConnectionRepository(userId, socialUserConnectionRepository, connectionFactoryLocator);
    };
}


package package.security.social;

import package.config.JHipsterProperties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.web.SignInAdapter;
import org.springframework.web.context.request.NativeWebRequest;

import javax.inject.Inject;

public class CustomSignInAdapter implements SignInAdapter {

    @SuppressWarnings("unused")
    private final Logger log = LoggerFactory.getLogger(CustomSignInAdapter.class);

    @Inject
    private UserDetailsService userDetailsService;

    @Inject
    private JHipsterProperties jHipsterProperties;

    @Override
    public String signIn(String userId, Connection<?> connection, NativeWebRequest request) {
        UserDetails user = userDetailsService.loadUserByUsername(userId);
        Authentication newAuth = new UsernamePasswordAuthenticationToken(
            user,
            null,
            user.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(newAuth);
        return jHipsterProperties.getSocial().getRedirectAfterSignIn();
    }
}

Example for JWT only

import io.github.jhipster.config.JHipsterProperties;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.web.SignInAdapter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.Cookie;

public class CustomSignInAdapter implements SignInAdapter {

    @SuppressWarnings("unused")
    private final Logger log = LoggerFactory.getLogger(CustomSignInAdapter.class);

    private final UserDetailsService userDetailsService;

    private final JHipsterProperties jHipsterProperties;

    private final TokenProvider tokenProvider;


    public CustomSignInAdapter(UserDetailsService userDetailsService, JHipsterProperties jHipsterProperties,
            TokenProvider tokenProvider) {
        this.userDetailsService = userDetailsService;
        this.jHipsterProperties = jHipsterProperties;
        this.tokenProvider = tokenProvider;
    }

    @Override
    public String signIn(String userId, Connection<?> connection, NativeWebRequest request){
        try {
            UserDetails user = userDetailsService.loadUserByUsername(userId);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                user,
                null,
                user.getAuthorities());

            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            String jwt = tokenProvider.createToken(authenticationToken, false);
            ServletWebRequest servletWebRequest = (ServletWebRequest) request;
            servletWebRequest.getResponse().addCookie(getSocialAuthenticationCookie(jwt));
        } catch (AuthenticationException ae) {
            log.error("Social authentication error");
            log.trace("Authentication exception trace: {}", ae);
        }
        return jHipsterProperties.getSocial().getRedirectAfterSignIn();
    }

    private Cookie getSocialAuthenticationCookie(String token) {
        Cookie socialAuthCookie = new Cookie("social-authentication", token);
        socialAuthCookie.setPath("/");
        socialAuthCookie.setMaxAge(10);
        return socialAuthCookie;
    }
}

JHipsterProperties is just layer before simple Properties configuration. This is generated by JHipster. You could generate monolithic application and see examples how backend and frontend should work to enable social buttons.

egorlitvinenko
  • 2,736
  • 2
  • 16
  • 36
  • Is this RESTful? It seems that it returns a redirection view after signing in the user. This makes it impossible to work in Ionic, no? – Alex Arvanitidis Sep 01 '17 at 08:22
  • For redirects you can use in-app-browser, isn't it? – egorlitvinenko Sep 01 '17 at 10:07
  • No, I want to send the login user token to the server, validate him, then log him in, and send back a response with the JWT token, all ready and logged in. No redirects in the front-end. – Alex Arvanitidis Sep 01 '17 at 10:09
  • Who does generate login user token? – egorlitvinenko Sep 01 '17 at 10:15
  • The FB.login() from the SDK or cordova generates a temporary token that can securely login the user on the server. No redirections and no in-app browser stuff. https://developers.facebook.com/docs/facebook-login/access-tokens. I've actually done this, but what I'm missing is with Spring Social, how to do the token validation, and log in the user on Spring Authentication module, also generate a JWT token for him. – Alex Arvanitidis Sep 01 '17 at 10:19
  • 1
    To validate token - just send him to https://graph.facebook.com/me for FB, for example, and on answer, create necessary JWT. You don't need Spring Social for it, just use Spring Security. But if you create token on frontend you can't guarantee that this is really your frontend generate the token. On your case I'm added special JWT tokens for client (register JWT token for your particular application). Then receive token from FB. Then send app JWT token and user acces token to server and validate both of them, and create new user-client JWT token. All under https. – egorlitvinenko Sep 01 '17 at 10:33
  • But still I think this is not very secure. See also https://stackoverflow.com/questions/8605703/how-to-verify-facebook-access-token – egorlitvinenko Sep 01 '17 at 10:35
  • You get the bounty sir! – Alex Arvanitidis Sep 01 '17 at 11:03
  • Glad to help! :) – egorlitvinenko Sep 01 '17 at 11:06
1

To secure data in Database you have to use any of the 'hashing' algorithms. I recommend to use HMAC SHA 256 algorithm. Hashing can't able to do decrypt the data. To do the hashing you need the key, you can make the key as either static or dynamic based on your need. Store the encrypted key in Database by using any of the RSA encryption algorithms and while hashing you can decrypt the key and pass in hashing method.

To do Hashing use apache HmacUtils.

String hashedPassword = HmacUtils.hmacSha1Hex(password,passwordkey);

To do encryption and decryption Please use below tutorial RSA Encryption and Decryption