diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d7d398 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# These are some examples of commonly ignored file patterns. +# You should customize this list as applicable to your project. +# Learn more about .gitignore: +# https://www.atlassian.com/git/tutorials/saving-changes/gitignore + +# Node artifact files +node_modules/ +dist/ + +# Compiled Java class files +*.class + +# Compiled Python bytecode +*.py[cod] + +# Log files +*.log + +# Package files +*.jar + +# Maven +target/ +dist/ + +# JetBrains IDE +.idea/ +*.iml +logs/ + +# Unit test reports +TEST*.xml + +# Generated by MacOS +.DS_Store + +# Generated by Windows +Thumbs.db + +# Applications +*.app +*.exe +*.war + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv + +/LOG_HOME_IS_UNDEFINED/app/ +/social-login-by-token.iml diff --git a/README.md b/README.md new file mode 100644 index 0000000..82c276d --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# Validate Social login token at back-end and get profile information + +# Social Login (IOS, Google, Facebook) +- Create Web Project in developer console of social platform +- Using developer console and get the access token then hit the API + + +- Facebook PlayGround + - https://developers.facebook.com/tools/explorer/ + + +- Google PlayGround + - https://developers.google.com/oauthplayground/ + - Scope : openid email profile + - Exchange authorization code for token then you will get id_token + + +- Swagger + - http://localhost:8080/app/swagger-ui.html + + +- For Facebook + - Request body + - { + "provider": "facebook", + "token": " Get access token from Playground " + } + + +- For Google + - Request body + - { + "provider": "google", + "token": " Get id token from Playground " + } + + +- For Apple : + - We need authorization code to validate on it. + - We are passing the values of firstName and lastName because when we get authorization code that time user-info will be provided after that only email will be received. This is current behaviour in APPLE doc. + - Request body + - { + "firstName" : "Hello" + "lastName" : "World" + "provider" : "apple", + "token" : "Get authorization code. It will be used only one time to validate and get the user information" + } + + +- Important class and points + - SocialClient.java + - google and facebook api to get user info + + - IOSConfig.java + - 4 properties we need from apple developer project + + - IOSClient.java + - get apple user info \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..8b9da3b --- /dev/null +++ b/mvnw @@ -0,0 +1,286 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + wget "$jarUrl" -O "$wrapperJarPath" + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + curl -o "$wrapperJarPath" "$jarUrl" + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..d562a74 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,161 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar" +FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + echo Found %WRAPPER_JAR% +) else ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')" + echo Finished downloading %WRAPPER_JAR% +) +@REM End of extension + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto errorType +goto end + +:errorType +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2c33d8e --- /dev/null +++ b/pom.xml @@ -0,0 +1,89 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.2.1.RELEASE + + + com.sm + social-login-be + 0.0.1-SNAPSHOT + social-login-be + Social Login validation using access token or authorization code + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.security + spring-security-oauth2-client + + + org.apache.commons + commons-lang3 + 3.9 + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + + io.springfox + springfox-bean-validators + 2.9.2 + + + + org.springframework.boot + spring-boot-starter-tomcat + + + javax.validation + validation-api + 2.0.1.Final + + + + org.hibernate.validator + hibernate-validator + 6.0.13.Final + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + social-login + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/main/java/com/sm/Application.java b/src/main/java/com/sm/Application.java new file mode 100644 index 0000000..53d0895 --- /dev/null +++ b/src/main/java/com/sm/Application.java @@ -0,0 +1,26 @@ +package com.sm; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.web.client.RestTemplate; + +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class }) +public class Application extends SpringBootServletInitializer { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + return application.sources(Application.class); + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/main/java/com/sm/common/dto/ErrorResponseDto.java b/src/main/java/com/sm/common/dto/ErrorResponseDto.java new file mode 100644 index 0000000..7772790 --- /dev/null +++ b/src/main/java/com/sm/common/dto/ErrorResponseDto.java @@ -0,0 +1,30 @@ +package com.sm.common.dto; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonAutoDetect +@JsonIgnoreProperties(ignoreUnknown = true) +public class ErrorResponseDto { + + private String errorCode; + private String message; + + public ErrorResponseDto(){} + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} \ No newline at end of file diff --git a/src/main/java/com/sm/common/error/dto/ErrorDto.java b/src/main/java/com/sm/common/error/dto/ErrorDto.java new file mode 100644 index 0000000..de0af96 --- /dev/null +++ b/src/main/java/com/sm/common/error/dto/ErrorDto.java @@ -0,0 +1,67 @@ +package com.sm.common.error.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.io.Serializable; +import java.time.Instant; + +public class ErrorDto implements Serializable { + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "GMT") + private Instant timestamp; + private String code; + private String message; + private String path; + @JsonIgnore + private String detailMessage; + + public ErrorDto(){} + + public ErrorDto(String code,String message,String path){ + this.timestamp = Instant.now(); + this.code = code; + this.message = message; + this.path = path; + } + + public Instant getTimestamp() { + return timestamp; + } + + public void setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getDetailMessage() { + return detailMessage; + } + + public void setDetailMessage(String detailMessage) { + this.detailMessage = detailMessage; + } +} diff --git a/src/main/java/com/sm/common/error/type/ErrorType.java b/src/main/java/com/sm/common/error/type/ErrorType.java new file mode 100644 index 0000000..fa9fcb4 --- /dev/null +++ b/src/main/java/com/sm/common/error/type/ErrorType.java @@ -0,0 +1,32 @@ +package com.sm.common.error.type; + + +public enum ErrorType { + + //Validation + NOT_BLANK("1000", "Please provide the value of %s"), + PROVIDER_NOT_EMPTY("1001", "Provider must be facebook, google or apple"), + TOKEN_BAD_REQUEST("1002", "Token is not valid or expire."); + + + private final String code; + private final String message; + + ErrorType(String code, String message) { + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public static ErrorType getErrorTypeByName(String name){ + return ErrorType.valueOf(name); + } + +} diff --git a/src/main/java/com/sm/common/exception/GenericErrorException.java b/src/main/java/com/sm/common/exception/GenericErrorException.java new file mode 100644 index 0000000..1c53ca2 --- /dev/null +++ b/src/main/java/com/sm/common/exception/GenericErrorException.java @@ -0,0 +1,47 @@ +package com.sm.common.exception; + + +public class GenericErrorException extends RuntimeException { + private String code; + private String message; + private Throwable cause; + + public GenericErrorException(String code, String message) { + super(message); + this.code = code; + this.message = message; + } + + public GenericErrorException(String code , String message, Throwable cause) { + super(message, cause); + this.code = code; + this.message = message; + this.cause = cause; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + @Override + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public Throwable getCause() { + return cause; + } + + public void setCause(Throwable cause) { + this.cause = cause; + } +} diff --git a/src/main/java/com/sm/common/exception/InternalServerException.java b/src/main/java/com/sm/common/exception/InternalServerException.java new file mode 100644 index 0000000..c0b0a43 --- /dev/null +++ b/src/main/java/com/sm/common/exception/InternalServerException.java @@ -0,0 +1,52 @@ +package com.sm.common.exception; + + +public class InternalServerException extends RuntimeException { + private String code; + private String message; + private Throwable cause; + + public InternalServerException(String code, String message) { + super(message); + this.code = code; + this.message = message; + } + + public InternalServerException(String code , String message, Throwable cause) { + super(message, cause); + this.code = code; + this.message = message; + this.cause = cause; + } + + public InternalServerException( Throwable cause) { + super(cause); + this.cause = cause; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + @Override + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public Throwable getCause() { + return cause; + } + + public void setCause(Throwable cause) { + this.cause = cause; + } +} diff --git a/src/main/java/com/sm/common/util/DateUtil.java b/src/main/java/com/sm/common/util/DateUtil.java new file mode 100644 index 0000000..19eaa80 --- /dev/null +++ b/src/main/java/com/sm/common/util/DateUtil.java @@ -0,0 +1,32 @@ +package com.sm.common.util; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Date; + +public class DateUtil { + + private static final String DATE_FORMAT_PATTERN = "yyyy/MM/dd"; + private static final DateFormat DATE_FORMAT = new SimpleDateFormat(DATE_FORMAT_PATTERN); + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); + + public static Date addMonthInCurrentDate(int month){ + LocalDate localDate = LocalDate.now(); + localDate = localDate.plusMonths(month); + Date date = Date.from(localDate.atStartOfDay( ZoneId.systemDefault()).toInstant()); + return date; + } + + public static long getDateDifference(String date){ + LocalDate currentDate = LocalDate.now(); + return ChronoUnit.DAYS.between(currentDate, LocalDate.parse(date, formatter)); + } + + public static String formatDateInString(Date date){ + return DATE_FORMAT.format(date); + } +} diff --git a/src/main/java/com/sm/common/util/FileUtil.java b/src/main/java/com/sm/common/util/FileUtil.java new file mode 100644 index 0000000..f702720 --- /dev/null +++ b/src/main/java/com/sm/common/util/FileUtil.java @@ -0,0 +1,29 @@ +package com.sm.common.util; + +import com.sm.common.exception.InternalServerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.util.FileCopyUtils; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class FileUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(FileUtil.class); + + public static String readFileFromClassPath(String fileName) throws InternalServerException { + String data = null; + try { + Resource resource = new ClassPathResource(fileName); + InputStream inputStream = resource.getInputStream(); + byte[] bdata = FileCopyUtils.copyToByteArray(inputStream); + data = new String(bdata, StandardCharsets.UTF_8); + } catch (Exception e) { + LOGGER.error("File Exception: ", e); + throw new InternalServerException(e); + } + return data; + } +} \ No newline at end of file diff --git a/src/main/java/com/sm/common/util/SecureStringUtil.java b/src/main/java/com/sm/common/util/SecureStringUtil.java new file mode 100644 index 0000000..3faad6e --- /dev/null +++ b/src/main/java/com/sm/common/util/SecureStringUtil.java @@ -0,0 +1,39 @@ +package com.sm.common.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class SecureStringUtil { + private static final Logger logger = LoggerFactory.getLogger(SecureStringUtil.class); + + private static final String STRING_SEED = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + private static SecureRandom SECURE_RANDOM; + + static { + try { + SECURE_RANDOM = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + logger.error("NoSuchAlgorithmException for secure random: "+e.getMessage(),e); + } + } + + public static String randomString(int length) { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < length; i++) { + int secureRandomIndex = SECURE_RANDOM.nextInt(STRING_SEED.length()); + sb.append(STRING_SEED.charAt(secureRandomIndex)); + } + + return sb.toString(); + } + + public static boolean equals(String first, String second) { + return MessageDigest.isEqual(first.getBytes(StandardCharsets.UTF_8), second.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/com/sm/config/SwaggerConfig.java b/src/main/java/com/sm/config/SwaggerConfig.java new file mode 100644 index 0000000..8dbd25f --- /dev/null +++ b/src/main/java/com/sm/config/SwaggerConfig.java @@ -0,0 +1,36 @@ +package com.sm.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + @Value("${swagger.enable}") + private Boolean swaggerEnable; + + @Bean + public Docket productApi() { + return new Docket(DocumentationType.SWAGGER_2) + .enable(swaggerEnable) + .select().apis(RequestHandlerSelectors.basePackage("com.sm.controller")) + .build() + .apiInfo(apiInfo()); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("Social Login") + .version("1.0") + .description("Validate social login access token and get profile information") + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/sm/config/social/SocialClient.java b/src/main/java/com/sm/config/social/SocialClient.java new file mode 100644 index 0000000..06922a0 --- /dev/null +++ b/src/main/java/com/sm/config/social/SocialClient.java @@ -0,0 +1,147 @@ +package com.sm.config.social; + +import com.sm.common.error.type.ErrorType; +import com.sm.common.exception.GenericErrorException; +import com.sm.common.util.DateUtil; +import com.sm.common.util.SecureStringUtil; +import com.sm.config.social.ios.IOSClient; +import com.sm.config.social.user.IOSInfo; +import com.sm.config.social.user.FacebookUser; +import com.sm.config.social.user.GoogleUser; +import com.sm.controller.dto.request.SocialRequestModel; +import com.sm.dto.response.user.UserDto; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class SocialClient { + private static final Logger logger = LoggerFactory.getLogger(SocialClient.class); + + // FACEBOOK + private final String FACEBOOK_GRAPH_API_BASE = "https://graph.facebook.com"; + private final String FACEBOOK_PATH = "/me?fields={fields}&redirect={redirect}&access_token={access_token}"; + private final String FACEBOOK_FIELDS= "id,gender,first_name,middle_name,last_name,name,email,verified,is_verified,picture.width(250).height(250)"; + + // GOOGLE + //private final String GOOGLE_PATH = "?scope={scope}&access_token={access_token}"; + private final String GOOGLE_PATH = "?id_token={id_token}"; + private final String GOOGLE_SCOPE = "email,profile"; + //private final String GOOGLE_API_BASE ="https://www.googleapis.com/oauth2/v3/userinfo"; + private final String GOOGLE_API_BASE ="https://oauth2.googleapis.com/tokeninfo"; + + @Autowired private RestTemplate restTemplate; + @Autowired private IOSClient appleClient; + + + /** + * fetch facebook user data and convert to UserDto + * @param accessToken + * @return + */ + + public UserDto getFacebookUser(String accessToken) { + // https://graph.facebook.com/v3.0/me?fields=id,first_name,middle_name,last_name,name,email,verified,is_verified,picture.width(250).height(250) + UserDto userDto = null; + try { + final Map variables = new HashMap<>(); + variables.put("fields", FACEBOOK_FIELDS); + variables.put("redirect", "false"); + variables.put("access_token", accessToken); + FacebookUser facebookUser = restTemplate.getForObject(FACEBOOK_GRAPH_API_BASE + FACEBOOK_PATH, FacebookUser.class, variables); + userDto = convertTo(facebookUser); + }catch (Exception e){ + logger.error("While fetching facebook data, getting Exception :",e); + throw new GenericErrorException(ErrorType.TOKEN_BAD_REQUEST.getCode(),ErrorType.TOKEN_BAD_REQUEST.getMessage(),e); + } + return userDto; + } + + /** + * fetch google user data and convert to UserDto + * @param accessToken + * @return + */ + public UserDto getGoogleUser(String accessToken) { + UserDto userDto = null; + // https://www.googleapis.com/oauth2/v3/userinfo?access_token={access_token} + try{ + // String scope = "https://www.googleapis.com/auth/userinfo.email,https://www.googleapis.com/auth/userinfo.profile,https://www.googleapis.com/auth/user.gender.read,openid"; + final Map variables = new HashMap<>(); + // variables.put("scope", GOOGLE_SCOPE); + //variables.put("access_token", accessToken); + variables.put("id_token", accessToken); + GoogleUser googleUser = restTemplate.getForObject(GOOGLE_API_BASE + GOOGLE_PATH, GoogleUser.class, variables); + userDto = convertTo(googleUser); + }catch (Exception e){ + logger.error("While fetching google data, getting Exception :",e); + throw new GenericErrorException(ErrorType.TOKEN_BAD_REQUEST.getCode(),ErrorType.TOKEN_BAD_REQUEST.getMessage(),e); + } + return userDto; + } + + /** + * fetch apple user data and convert to UserDto + * @param socialRequestModel + * @return + */ + public UserDto getAppleUser(SocialRequestModel socialRequestModel,String clientSecret) { + UserDto userDto = null; + + // if secret receive null then create new secret with expiry date of 3 months. + if(clientSecret == null) { + clientSecret = appleClient.getClientSecret(); + // expiry 3 month + Date addedDate = DateUtil.addMonthInCurrentDate(3); + // save Apple expiry date and secret in db + } + + IOSInfo appleInfo =appleClient.retrieveData(clientSecret, socialRequestModel.getToken()); + userDto = convertTo(appleInfo); + + if(socialRequestModel.getFirstName() != null) + userDto.setFirstName(socialRequestModel.getFirstName()); + if(socialRequestModel.getLastName() != null) + userDto.setLastName(socialRequestModel.getLastName()); + + return userDto; + } + + private UserDto convertTo(FacebookUser facebookUser) { + UserDto userDto = new UserDto(); + userDto.setProviderId(facebookUser.getId()); + userDto.setProvider("facebook"); + userDto.setEmail(facebookUser.getEmail()); + userDto.setFirstName(facebookUser.getFirstName()); + userDto.setLastName(facebookUser.getLastName()); + userDto.setPassword(SecureStringUtil.randomString(30)); + return userDto; + } + + private UserDto convertTo(GoogleUser googleUser) { + UserDto userDto = new UserDto(); + userDto.setProviderId(googleUser.getSub()); + userDto.setProvider("google"); + userDto.setEmail(googleUser.getEmail()); + userDto.setFirstName(googleUser.getGivenName()); + userDto.setLastName(googleUser.getFamilyName()); + userDto.setPassword(SecureStringUtil.randomString(30)); + userDto.setPicture(googleUser.getPicture()); + return userDto; + } + + private UserDto convertTo(IOSInfo appleInfo) { + UserDto userDto = new UserDto(); + userDto.setProviderId(appleInfo.getId()); + userDto.setProvider("ios"); + userDto.setEmail(appleInfo.getEmail()); + userDto.setPassword(SecureStringUtil.randomString(30)); + return userDto; + } +} diff --git a/src/main/java/com/sm/config/social/ios/DecodeResponse.java b/src/main/java/com/sm/config/social/ios/DecodeResponse.java new file mode 100644 index 0000000..e03c6fd --- /dev/null +++ b/src/main/java/com/sm/config/social/ios/DecodeResponse.java @@ -0,0 +1,99 @@ +package com.sm.config.social.ios; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class DecodeResponse { + public String iss; + public String aud; + public int exp; + public int iat; + public String sub; + public String at_hash; + public String email; + public String email_verified; + public int auth_time; + public boolean nonce_supported; + + public DecodeResponse(){} + + public String getIss() { + return iss; + } + + public void setIss(String iss) { + this.iss = iss; + } + + public String getAud() { + return aud; + } + + public void setAud(String aud) { + this.aud = aud; + } + + public int getExp() { + return exp; + } + + public void setExp(int exp) { + this.exp = exp; + } + + public int getIat() { + return iat; + } + + public void setIat(int iat) { + this.iat = iat; + } + + public String getSub() { + return sub; + } + + public void setSub(String sub) { + this.sub = sub; + } + + public String getAt_hash() { + return at_hash; + } + + public void setAt_hash(String at_hash) { + this.at_hash = at_hash; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getEmail_verified() { + return email_verified; + } + + public void setEmail_verified(String email_verified) { + this.email_verified = email_verified; + } + + public int getAuth_time() { + return auth_time; + } + + public void setAuth_time(int auth_time) { + this.auth_time = auth_time; + } + + public boolean isNonce_supported() { + return nonce_supported; + } + + public void setNonce_supported(boolean nonce_supported) { + this.nonce_supported = nonce_supported; + } +} diff --git a/src/main/java/com/sm/config/social/ios/IOSClient.java b/src/main/java/com/sm/config/social/ios/IOSClient.java new file mode 100644 index 0000000..8c33d5c --- /dev/null +++ b/src/main/java/com/sm/config/social/ios/IOSClient.java @@ -0,0 +1,133 @@ +package com.sm.config.social.ios; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import com.sm.common.error.type.ErrorType; +import com.sm.common.exception.GenericErrorException; +import com.sm.common.util.DateUtil; +import com.sm.common.util.FileUtil; +import com.sm.config.social.user.IOSInfo; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Date; + +@Service +public class IOSClient { + private static final Logger LOGGER = LoggerFactory.getLogger(IOSClient.class); + + private static final String APPLE_AUTH_URL = "https://appleid.apple.com/auth/token"; + private static final String RESOURCE_FOLDER_NAME = "ios/"; + + @Autowired RestTemplate restTemplate; + @Autowired IOSConfig appleConfig; + + public String getClientSecret() throws GenericErrorException { + String inputKeyFile = getFileData(); + + String clientSecret = ""; + try { + final byte[] keyBytes = java.util.Base64.getDecoder().decode(inputKeyFile); + final KeyFactory keyFactory = KeyFactory.getInstance("EC"); + final PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); + + + // see https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens + final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256) + .keyID(appleConfig.getKid()) + .build(); + + final JWTClaimsSet claimsSet = createClaim(); + + clientSecret = generateClientSecret((java.security.interfaces.ECPrivateKey) privateKey, + header, claimsSet); + + } catch (Exception e) { + LOGGER.error("Exception while creating secret of apple :",e); + throw new GenericErrorException(ErrorType.TOKEN_BAD_REQUEST.getCode(),ErrorType.TOKEN_BAD_REQUEST.getMessage(),e); + } + return clientSecret; + } + + private String generateClientSecret(java.security.interfaces.ECPrivateKey privateKey, JWSHeader header, JWTClaimsSet claimsSet) throws JOSEException { + String clientSecret; + SignedJWT signedJWT = new SignedJWT(header, claimsSet); + JWSSigner signer = new ECDSASigner(privateKey); + signedJWT.sign(signer); + clientSecret = signedJWT.serialize(); + return clientSecret; + } + + private JWTClaimsSet createClaim() { + // expiry 3 month + final Date timestamp = new Date(); + final Date addedDate = DateUtil.addMonthInCurrentDate(3); + + return new JWTClaimsSet.Builder() + .issuer(appleConfig.getTeamId()) + .issueTime(timestamp) + .expirationTime(addedDate) + .audience("https://appleid.apple.com") + .subject(appleConfig.getClientId()) + .build(); + } + + private String getFileData(){ + String inputKeyFile = FileUtil.readFileFromClassPath(RESOURCE_FOLDER_NAME + appleConfig.getFileName()); + inputKeyFile = inputKeyFile.replaceFirst("-----BEGIN PRIVATE KEY-----", ""); + inputKeyFile = inputKeyFile.replaceFirst("-----END PRIVATE KEY-----", ""); + inputKeyFile = inputKeyFile.replaceAll("\\s", ""); + return inputKeyFile; + } + + public IOSInfo retrieveData(String clientSecret, String authorizationCode) throws GenericErrorException{ + IOSInfo iosInfo = new IOSInfo(); + try { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("client_id", appleConfig.getClientId()); // app_id like com.app.id + map.add("client_secret", clientSecret); + map.add("grant_type", "authorization_code"); + map.add("code", authorizationCode); // JWT code we got from iOS + HttpEntity> request = new HttpEntity<>(map, headers); + + TokenResponse response = restTemplate.postForObject(APPLE_AUTH_URL, request, TokenResponse.class); + if(response != null) { + String idToken = response.getId_token(); + if(idToken != null) { + String payload = idToken.split("\\.")[1];// 0 is header we ignore it for now + String decode = new String(Base64.getDecoder().decode(payload)); + ObjectMapper mapper = new ObjectMapper(); + DecodeResponse decodeResponse = mapper.readValue(decode, DecodeResponse.class); + + iosInfo.setEmail(decodeResponse.getEmail() != null ? decodeResponse.getEmail() : StringUtils.EMPTY); + iosInfo.setId(decodeResponse.getSub()); + } + } + } catch (Exception exception) { + LOGGER.error("Exception while retrieving data of apple user:",exception); + throw new GenericErrorException(ErrorType.TOKEN_BAD_REQUEST.getCode(),ErrorType.TOKEN_BAD_REQUEST.getMessage(),exception); + } + return iosInfo; + } +} diff --git a/src/main/java/com/sm/config/social/ios/IOSConfig.java b/src/main/java/com/sm/config/social/ios/IOSConfig.java new file mode 100644 index 0000000..8aaaaab --- /dev/null +++ b/src/main/java/com/sm/config/social/ios/IOSConfig.java @@ -0,0 +1,36 @@ +package com.sm.config.social.ios; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class IOSConfig { + + @Value("${ios.client_id}") + private String clientId; + + @Value("${ios.team_id}") + private String teamId; + + @Value("${ios.kid}") + private String kid; + + @Value("${ios.file_name}") + private String fileName; + + public String getClientId() { + return clientId; + } + + public String getTeamId() { + return teamId; + } + + public String getKid() { + return kid; + } + + public String getFileName() { + return fileName; + } +} diff --git a/src/main/java/com/sm/config/social/ios/TokenResponse.java b/src/main/java/com/sm/config/social/ios/TokenResponse.java new file mode 100644 index 0000000..f6608cb --- /dev/null +++ b/src/main/java/com/sm/config/social/ios/TokenResponse.java @@ -0,0 +1,52 @@ +package com.sm.config.social.ios; + +public class TokenResponse { + + private String access_token; + private String token_type; + private Long expires_in; + private String refresh_token; + private String id_token; + + public TokenResponse(){} + + public String getAccess_token() { + return access_token; + } + + public void setAccess_token(String access_token) { + this.access_token = access_token; + } + + public String getToken_type() { + return token_type; + } + + public void setToken_type(String token_type) { + this.token_type = token_type; + } + + public Long getExpires_in() { + return expires_in; + } + + public void setExpires_in(Long expires_in) { + this.expires_in = expires_in; + } + + public String getRefresh_token() { + return refresh_token; + } + + public void setRefresh_token(String refresh_token) { + this.refresh_token = refresh_token; + } + + public String getId_token() { + return id_token; + } + + public void setId_token(String id_token) { + this.id_token = id_token; + } +} diff --git a/src/main/java/com/sm/config/social/user/FacebookUser.java b/src/main/java/com/sm/config/social/user/FacebookUser.java new file mode 100644 index 0000000..4893a14 --- /dev/null +++ b/src/main/java/com/sm/config/social/user/FacebookUser.java @@ -0,0 +1,68 @@ +package com.sm.config.social.user; + +import com.fasterxml.jackson.annotation.JsonProperty; + + +import java.util.Map; + +public class FacebookUser { + private String id; + @JsonProperty("first_name") + private String firstName; + @JsonProperty("last_name") + private String lastName; + private String email; + private String gender; + Map attributes; + // private FacebookPicture picture; + + public FacebookUser(){} + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getGender() { + return gender; + } + + public void setGender(String gender) { + this.gender = gender; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } +} \ No newline at end of file diff --git a/src/main/java/com/sm/config/social/user/GoogleUser.java b/src/main/java/com/sm/config/social/user/GoogleUser.java new file mode 100644 index 0000000..69b20e2 --- /dev/null +++ b/src/main/java/com/sm/config/social/user/GoogleUser.java @@ -0,0 +1,82 @@ +package com.sm.config.social.user; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class GoogleUser { + private String sub; + private String email; + private String gender; + @JsonProperty("given_name") + private String givenName; + private String name; + @JsonProperty("family_name") + private String familyName; + private String rawUserInfo; + private String picture; + + public GoogleUser(){} + + public String getSub() { + return sub; + } + + public void setSub(String sub) { + this.sub = sub; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getGender() { + return gender; + } + + public void setGender(String gender) { + this.gender = gender; + } + + public String getGivenName() { + return givenName; + } + + public void setGivenName(String givenName) { + this.givenName = givenName; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getFamilyName() { + return familyName; + } + + public void setFamilyName(String familyName) { + this.familyName = familyName; + } + + public String getRawUserInfo() { + return rawUserInfo; + } + + public void setRawUserInfo(String rawUserInfo) { + this.rawUserInfo = rawUserInfo; + } + + public String getPicture() { + return picture; + } + + public void setPicture(String picture) { + this.picture = picture; + } +} \ No newline at end of file diff --git a/src/main/java/com/sm/config/social/user/IOSInfo.java b/src/main/java/com/sm/config/social/user/IOSInfo.java new file mode 100644 index 0000000..643ec95 --- /dev/null +++ b/src/main/java/com/sm/config/social/user/IOSInfo.java @@ -0,0 +1,42 @@ +package com.sm.config.social.user; + +public class IOSInfo { + private String id; + private String firstName; + private String lastName; + private String email; + + public IOSInfo(){} + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/src/main/java/com/sm/controller/UserController.java b/src/main/java/com/sm/controller/UserController.java new file mode 100644 index 0000000..3d43a99 --- /dev/null +++ b/src/main/java/com/sm/controller/UserController.java @@ -0,0 +1,60 @@ +package com.sm.controller; + +import com.sm.common.error.dto.ErrorDto; +import com.sm.common.error.type.ErrorType; +import com.sm.common.exception.GenericErrorException; +import com.sm.controller.dto.request.SocialRequestModel; +import com.sm.dto.response.user.UserDto; +import com.sm.service.UserService; +import io.swagger.annotations.*; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + + +@RestController +@RequestMapping("/users") +@Api(value = "Social Login Resource") +public class UserController { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class); + + @Autowired + UserService userService; + + + @PostMapping("/login") + @ApiOperation(value = "Validate with social provider and get user-info", httpMethod = "POST") + @ApiResponses(value = { + @ApiResponse(response = UserDto.class, code = 200, message = ""), + @ApiResponse(response = ErrorDto.class, code = 404, message = ""), + @ApiResponse(response = ErrorDto.class, code = 500, message = "Internal server error")}) + public UserDto checkLogin(@Valid @RequestBody SocialRequestModel socialRequestModel) throws GenericErrorException { + validateForIOS(socialRequestModel); + return userService.validateAccessToken(socialRequestModel); + } + + /** + * Validate for ios : first name and last name + * @param socialRequestModel + */ + private void validateForIOS(SocialRequestModel socialRequestModel) { + + if (socialRequestModel.getProvider().equalsIgnoreCase("ios")) { + boolean flagFirstName = StringUtils.isBlank(socialRequestModel.getFirstName()); + + if (flagFirstName || StringUtils.isBlank(socialRequestModel.getLastName())) { + String value = (flagFirstName ? "first name" : " last name"); + throw new GenericErrorException(ErrorType.NOT_BLANK.getCode(), + String.format(ErrorType.NOT_BLANK.getMessage(), value)); + } + } + } +} diff --git a/src/main/java/com/sm/controller/dto/request/SocialRequestModel.java b/src/main/java/com/sm/controller/dto/request/SocialRequestModel.java new file mode 100644 index 0000000..2c7b239 --- /dev/null +++ b/src/main/java/com/sm/controller/dto/request/SocialRequestModel.java @@ -0,0 +1,53 @@ +package com.sm.controller.dto.request; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +public class SocialRequestModel { + @NotBlank(message="NOT_BLANK") + @NotEmpty(message="NOT_BLANK") + private String token; + + @NotBlank(message="NOT_BLANK") + @NotEmpty(message="NOT_BLANK") + @Pattern(regexp = "^(facebook|google|apple)$",flags = Pattern.Flag.CASE_INSENSITIVE, message = "PROVIDER_NOT_EMPTY") + private String provider; + + private String firstName; + private String lastName; + + public SocialRequestModel(){} + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } +} diff --git a/src/main/java/com/sm/dto/response/GenericResponse.java b/src/main/java/com/sm/dto/response/GenericResponse.java new file mode 100644 index 0000000..189095a --- /dev/null +++ b/src/main/java/com/sm/dto/response/GenericResponse.java @@ -0,0 +1,31 @@ +package com.sm.dto.response; + + +import java.io.Serializable; + +public class GenericResponse implements Serializable { + + public static final String REQUEST_SUCCESSFUL_MESSAGE = "Request completed successfully"; + private static final long serialVersionUID = 6835192601898364280L; + + private String message = REQUEST_SUCCESSFUL_MESSAGE; + private Object data; + + public GenericResponse(){} + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } +} diff --git a/src/main/java/com/sm/dto/response/user/UserDto.java b/src/main/java/com/sm/dto/response/user/UserDto.java new file mode 100644 index 0000000..4049452 --- /dev/null +++ b/src/main/java/com/sm/dto/response/user/UserDto.java @@ -0,0 +1,104 @@ +package com.sm.dto.response.user; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.io.Serializable; +import java.time.LocalDateTime; + +public class UserDto implements Serializable { + + private static final long serialVersionUID = 6835192601898364280L; + private Long id; + private String firstName; + private String lastName; + private String email; + @JsonIgnore + private String password; + private String provider; + private String providerId; + private Integer age; + private String gender; + private String picture; + + public UserDto(){} + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + public String getProviderId() { + return providerId; + } + + public void setProviderId(String providerId) { + this.providerId = providerId; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public String getGender() { + return gender; + } + + public void setGender(String gender) { + this.gender = gender; + } + + public String getPicture() { + return picture; + } + + public void setPicture(String picture) { + this.picture = picture; + } +} diff --git a/src/main/java/com/sm/service/UserService.java b/src/main/java/com/sm/service/UserService.java new file mode 100644 index 0000000..2f6a624 --- /dev/null +++ b/src/main/java/com/sm/service/UserService.java @@ -0,0 +1,9 @@ +package com.sm.service; + +import com.sm.common.exception.GenericErrorException; +import com.sm.controller.dto.request.*; +import com.sm.dto.response.user.UserDto; + +public interface UserService { + UserDto validateAccessToken(SocialRequestModel token) throws GenericErrorException; +} \ No newline at end of file diff --git a/src/main/java/com/sm/service/impl/UserServiceImpl.java b/src/main/java/com/sm/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..ec8c1d2 --- /dev/null +++ b/src/main/java/com/sm/service/impl/UserServiceImpl.java @@ -0,0 +1,55 @@ +package com.sm.service.impl; + + +import com.sm.common.exception.GenericErrorException; +import com.sm.config.social.SocialClient; +import com.sm.controller.dto.request.*; +import com.sm.dto.response.user.UserDto; +import com.sm.service.UserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + + +@Service +public class UserServiceImpl implements UserService { + private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class); + + @Autowired SocialClient socialClient; + + @Override + public UserDto validateAccessToken(SocialRequestModel socialRequestModel) throws GenericErrorException { + UserDto userDto = new UserDto(); + String provider = socialRequestModel.getProvider().toLowerCase().trim(); + + if (provider.equalsIgnoreCase("facebook")) { + userDto = socialClient.getFacebookUser(socialRequestModel.getToken()); + + } else if (provider.equalsIgnoreCase("google")) { + userDto = socialClient.getGoogleUser(socialRequestModel.getToken()); + + } else if (provider.equalsIgnoreCase("ios")) { +/* * save the client secret and expiry date in db for apple on the first time when we create it. + * expiry date will be set for 3 month while creating client secret. + * next time check that secret is present and the expiry date then fetch it + expiry date is compared with today's date if todays date is passed with expiry date then + create new secret and expiry date and save in db so there will be no call in every request. + * reusing same secret to call ios api + */ + + // implement above comments to set the value in clientSecret. Initially, it will be null + String clientSecret = null; + userDto = socialClient.getAppleUser(socialRequestModel, clientSecret); + } + UserDto userResponse = createUserSocial(userDto); + return userResponse; + } + + UserDto createUserSocial(UserDto user) throws GenericErrorException { + // save that user in DB + // unqiue will be provider id and provider + // provider : facebook,apple,google + return user; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..52d09ab --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,12 @@ +# REST +server.servlet.context-path=/app + +logging.config=classpath:logback-local.xml + +#ios +ios.client_id=${IOS_CLIENT_ID:com.hello} +ios.team_id=${IOS_TEAM_ID:ABCDEFG} +ios.kid=${IOS_KID_ID:TYYYUUUII} +ios.file_name=${IOS_FILE:ios-key.p8} + +swagger.enable=true diff --git a/src/main/resources/facebook playground.PNG b/src/main/resources/facebook playground.PNG new file mode 100644 index 0000000..348a4d0 Binary files /dev/null and b/src/main/resources/facebook playground.PNG differ diff --git a/src/main/resources/google playground.PNG b/src/main/resources/google playground.PNG new file mode 100644 index 0000000..42062af Binary files /dev/null and b/src/main/resources/google playground.PNG differ diff --git a/src/main/resources/ios/ios-key.p8 b/src/main/resources/ios/ios-key.p8 new file mode 100644 index 0000000..95d09f2 --- /dev/null +++ b/src/main/resources/ios/ios-key.p8 @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/src/main/resources/logback-local.xml b/src/main/resources/logback-local.xml new file mode 100644 index 0000000..8a6dd98 --- /dev/null +++ b/src/main/resources/logback-local.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + INFO + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + ${LOG_FILE}.log + + + ${ROTATED_LOG_FILE}.%d{yyyy-MM-dd}.%i.log + + 50MB + 30 + 500MB + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + + + + + + diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..e69de29