Skip to content

Commit

Permalink
Merge pull request #65 from Hendrik2319/add-google-login
Browse files Browse the repository at this point in the history
Add Google Login
  • Loading branch information
Hendrik2319 authored Dec 2, 2023
2 parents 7f328bb + 03f2219 commit be32ce1
Show file tree
Hide file tree
Showing 24 changed files with 1,002 additions and 556 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
* client id from OAuth2 app in GitHub
* `OAUTH_GITHUB_CLIENT_SECRET`
* client secret from OAuth2 app in GitHub
* `OAUTH_GOOGLE_CLIENT_ID`
* client id from OAuth2 app in Google
* `OAUTH_GOOGLE_CLIENT_SECRET`
* client secret from OAuth2 app in Google
* `INITIAL_ADMIN`
* user id of first admin: `github{ ID of GitHub Account }`
* to have an admin if user database is initially empty
Expand Down
2 changes: 1 addition & 1 deletion backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.6</version>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>net.schwarzbaer.spring.promptoptimizer</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package net.schwarzbaer.spring.promptoptimizer.backend.security;

import net.schwarzbaer.spring.promptoptimizer.backend.security.models.Role;
import net.schwarzbaer.spring.promptoptimizer.backend.security.models.StoredUserInfo;
import net.schwarzbaer.spring.promptoptimizer.backend.security.services.StoredUserInfoService;
import static org.springframework.security.config.Customizer.withDefaults;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -22,9 +27,10 @@
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;

import java.util.*;

import static org.springframework.security.config.Customizer.withDefaults;
import net.schwarzbaer.spring.promptoptimizer.backend.security.models.Role;
import net.schwarzbaer.spring.promptoptimizer.backend.security.models.StoredUserInfo;
import net.schwarzbaer.spring.promptoptimizer.backend.security.services.StoredUserInfoService;
import net.schwarzbaer.spring.promptoptimizer.backend.security.services.UserAttributesService;

@Configuration
@EnableWebSecurity
Expand Down Expand Up @@ -80,26 +86,40 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
}

@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(StoredUserInfoService storedUserInfoService) {
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService(StoredUserInfoService storedUserInfoService, UserAttributesService userAttributesService) {
DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
return request -> configureUserData(storedUserInfoService, delegate, request);
return request ->
configureUserData(storedUserInfoService, userAttributesService, delegate, request);
}

DefaultOAuth2User configureUserData(StoredUserInfoService storedUserInfoService, DefaultOAuth2UserService delegate, OAuth2UserRequest request) {
DefaultOAuth2User configureUserData(
StoredUserInfoService storedUserInfoService,
UserAttributesService userAttributesService,
DefaultOAuth2UserService delegate,
OAuth2UserRequest request
) {
OAuth2User user = delegate.loadUser(request);
Collection<GrantedAuthority> newAuthorities = new ArrayList<>(user.getAuthorities());
Map<String, Object> newAttributes = new HashMap<>(user.getAttributes());

String registrationId = request.getClientRegistration().getRegistrationId();
String userDbId = registrationId + user.getName();
newAttributes.put("UserDbId", userDbId);

// System.out.println("User: ["+ registrationId +"] "+ user.getName());
// newAttributes.forEach((key, value) ->
// System.out.println(" ["+key+"]: "+value+ (value==null ? "" : " { Class:"+value.getClass().getName()+" }"))
// );

newAttributes.put(UserAttributesService.ATTR_USER_DB_ID, userDbId);
newAttributes.put(UserAttributesService.ATTR_REGISTRATION_ID, registrationId);
Role role = null;
userAttributesService.fixAttributesIfNeeded(newAttributes, registrationId);

final Optional<StoredUserInfo> storedUserInfoOpt = storedUserInfoService.getUserById(userDbId);
if (storedUserInfoOpt.isPresent()) {
final StoredUserInfo storedUserInfo = storedUserInfoOpt.get();
role = storedUserInfo.role();
storedUserInfoService.updateUserIfNeeded(storedUserInfo, newAttributes);
storedUserInfoService.updateUserIfNeeded(storedUserInfo, registrationId, newAttributes);
}

if (role==null && initialAdmin.equals(userDbId))
Expand All @@ -109,10 +129,10 @@ DefaultOAuth2User configureUserData(StoredUserInfoService storedUserInfoService,
role = Role.UNKNOWN_ACCOUNT;

if (storedUserInfoOpt.isEmpty())
storedUserInfoService.addUser(role, registrationId, newAttributes);
storedUserInfoService.addUser(userDbId, registrationId, role, newAttributes);

newAuthorities.add(new SimpleGrantedAuthority(role.getLong()));
return new DefaultOAuth2User(newAuthorities, newAttributes, "id");
return new DefaultOAuth2User(newAuthorities, newAttributes, UserAttributesService.ATTR_USER_DB_ID);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class StoredUserInfoService {

private final StoredUserInfoRepository storedUserInfoRepository;
private final UserService userService;
private final UserAttributesService userAttributesService;

// ####################################################################################
// Called by SecurityConfig
Expand All @@ -26,34 +27,42 @@ public Optional<StoredUserInfo> getUserById(String userDbId) {
return storedUserInfoRepository.findById(userDbId);
}

public void addUser(Role role, String registrationId, Map<String, Object> newAttributes) {
public void addUser(String userDbId, String registrationId, Role role, Map<String, Object> newAttributes) {
userDbId = Objects.requireNonNull(userDbId);
role = Objects.requireNonNull(role);
newAttributes = Objects.requireNonNull(newAttributes);

storedUserInfoRepository.save(new StoredUserInfo(
Objects.toString(newAttributes.get(UserService.ATTR_USER_DB_ID ), null),
userDbId,
role,
registrationId,
Objects.toString(newAttributes.get(UserService.ATTR_ORIGINAL_ID), null),
Objects.toString(newAttributes.get(UserService.ATTR_LOGIN ), null),
Objects.toString(newAttributes.get(UserService.ATTR_NAME ), null),
Objects.toString(newAttributes.get(UserService.ATTR_LOCATION ), null),
Objects.toString(newAttributes.get(UserService.ATTR_URL ), null),
Objects.toString(newAttributes.get(UserService.ATTR_AVATAR_URL ), null),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.ORIGINAL_ID, null ),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.LOGIN , null ),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.NAME , null ),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.LOCATION , null ),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.URL , null ),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.AVATAR_URL , null ),
null
));
}

public void updateUserIfNeeded(StoredUserInfo storedUserInfo, Map<String, Object> newAttributes) {
public void updateUserIfNeeded(StoredUserInfo storedUserInfo, String registrationId, Map<String, Object> newAttributes) {
storedUserInfo = Objects.requireNonNull(storedUserInfo);
newAttributes = Objects.requireNonNull(newAttributes);

StoredUserInfo updatedUserInfo = new StoredUserInfo(
storedUserInfo.id(),
storedUserInfo.role(),
storedUserInfo.registrationId(),
Objects.toString(newAttributes.get(UserService.ATTR_ORIGINAL_ID), storedUserInfo.originalId()),
Objects.toString(newAttributes.get(UserService.ATTR_LOGIN ), storedUserInfo.login ()),
Objects.toString(newAttributes.get(UserService.ATTR_NAME ), storedUserInfo.name ()),
Objects.toString(newAttributes.get(UserService.ATTR_LOCATION ), storedUserInfo.location ()),
Objects.toString(newAttributes.get(UserService.ATTR_URL ), storedUserInfo.url ()),
Objects.toString(newAttributes.get(UserService.ATTR_AVATAR_URL ), storedUserInfo.avatar_url()),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.ORIGINAL_ID, storedUserInfo.originalId()),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.LOGIN , storedUserInfo.login ()),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.NAME , storedUserInfo.name ()),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.LOCATION , storedUserInfo.location ()),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.URL , storedUserInfo.url ()),
userAttributesService.getAttribute( newAttributes, registrationId, UserAttributesService.Field.AVATAR_URL , storedUserInfo.avatar_url()),
storedUserInfo.denialReason()
);

if (!updatedUserInfo.equals(storedUserInfo))
storedUserInfoRepository.save(updatedUserInfo);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package net.schwarzbaer.spring.promptoptimizer.backend.security.services;

import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.springframework.lang.NonNull;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UserAttributesService {

public static final String ATTR_USER_DB_ID = "UserDbId";
public static final String ATTR_REGISTRATION_ID = "RegistrationId";

public enum Field {
ORIGINAL_ID,
LOGIN ,
NAME ,
LOCATION ,
URL ,
AVATAR_URL ,
}

public enum Registration {
GITHUB("github"),
GOOGLE("google"),
;
public final String id;
private Registration(String id) {
this.id = id;
}
}


private static final Map<String, Map<Field, String>> config = createConfig();

private static Map<String, Map<Field, String>> createConfig() {
HashMap<String, Map<Field, String>> newConfig = new HashMap<>();
EnumMap<Field, String> fields;

fields = new EnumMap<>(Field.class);
fields.put( Field.ORIGINAL_ID, "id" );
fields.put( Field.LOGIN , "login" );
fields.put( Field.NAME , "name" );
fields.put( Field.LOCATION , "location" );
fields.put( Field.URL , "html_url" );
fields.put( Field.AVATAR_URL , "avatar_url" );
newConfig.put(Registration.GITHUB.id, fields);

fields = new EnumMap<>(Field.class);
fields.put( Field.ORIGINAL_ID, "original_Id");
fields.put( Field.LOGIN , "email" );
fields.put( Field.NAME , "name" );
fields.put( Field.LOCATION , "locale" );
// fields.put( Field.URL , "html_url" );
fields.put( Field.AVATAR_URL , "picture" );
newConfig.put(Registration.GOOGLE.id, fields);

return newConfig;
}


public void fixAttributesIfNeeded(Map<String, Object> attributes, String registrationId) {
if (Registration.GOOGLE.id.equals(registrationId)) {
attributes.put("original_Id", attributes.get("sub"));
}
}


public String getAttribute( @NonNull OAuth2AuthenticatedPrincipal user, @NonNull String field, String nullDefault )
{
return Objects.toString( user.getAttribute(field), nullDefault );
}

public String getAttribute( @NonNull Map<String, Object> userAttributes, @NonNull String field, String nullDefault )
{
return Objects.toString( userAttributes.get(field), nullDefault );
}

public String getAttribute( @NonNull OAuth2AuthenticatedPrincipal user, String registrationId, Field field, String nullDefault )
{
return getAttribute( user, registrationId, field, nullDefault, this::getAttribute );
}

public String getAttribute( @NonNull Map<String, Object> userAttributes, String registrationId, Field field, String nullDefault )
{
return getAttribute( userAttributes, registrationId, field, nullDefault, this::getAttribute );
}


private interface GetAttributeFunction<S> {
String getAttribute( @NonNull S source, @NonNull String field, String nullDefault);
}

private <S> String getAttribute( @NonNull S source, String registrationId, Field field, String nullDefault, GetAttributeFunction<S> getAttribute )
{
Map<Field, String> attrNames = config.get(registrationId);
if (attrNames==null) return nullDefault;

String attrName = attrNames.get(field);
if (attrName==null) return nullDefault;

return getAttribute.getAttribute( source, attrName, nullDefault );
}
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,21 @@
package net.schwarzbaer.spring.promptoptimizer.backend.security.services;

import lombok.RequiredArgsConstructor;
import net.schwarzbaer.spring.promptoptimizer.backend.security.models.Role;
import net.schwarzbaer.spring.promptoptimizer.backend.security.models.UserInfo;
import org.springframework.lang.NonNull;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.stereotype.Service;

import java.util.Objects;
import lombok.RequiredArgsConstructor;
import net.schwarzbaer.spring.promptoptimizer.backend.security.models.Role;
import net.schwarzbaer.spring.promptoptimizer.backend.security.models.UserInfo;

@Service
@RequiredArgsConstructor
public class UserService {

static final String ATTR_ORIGINAL_ID = "id";
static final String ATTR_USER_DB_ID = "UserDbId";
static final String ATTR_LOGIN = "login";
static final String ATTR_NAME = "name";
static final String ATTR_LOCATION = "location";
static final String ATTR_URL = "html_url";
static final String ATTR_AVATAR_URL = "avatar_url";

// @SuppressWarnings("java:S106")
// private static final PrintStream DEBUG_OUT = System.out;
private final UserAttributesService userAttributesService;

// ####################################################################################
// Called by and allowed for all users (authorized or not)
Expand All @@ -34,25 +24,21 @@ public class UserService {
public @NonNull UserInfo getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication!=null ? authentication.getPrincipal() : null;
// if (principal!=null) DEBUG_OUT.println("Principal: "+principal.getClass()+" -> "+principal);

if (principal instanceof OAuth2AuthenticatedPrincipal user) {
// DEBUG_OUT.println("User Attributes:");
// user.getAttributes().forEach((key, value) ->
// DEBUG_OUT.println(" ["+key+"]: "+value+ (value==null ? "" : " { Class:"+value.getClass().getName()+" }"))
// );
String registrationId = userAttributesService.getAttribute( user, UserAttributesService.ATTR_REGISTRATION_ID, null );

return new UserInfo(
true,
hasRole(user, Role.USER),
hasRole(user, Role.ADMIN),
Objects.toString( user.getAttribute(ATTR_ORIGINAL_ID), null ),
Objects.toString( user.getAttribute(ATTR_USER_DB_ID ), null ),
Objects.toString( user.getAttribute(ATTR_LOGIN ), null ),
Objects.toString( user.getAttribute(ATTR_NAME ), null ),
Objects.toString( user.getAttribute(ATTR_LOCATION ), null ),
Objects.toString( user.getAttribute(ATTR_URL ), null ),
Objects.toString( user.getAttribute(ATTR_AVATAR_URL ), null )
userAttributesService.getAttribute( user, registrationId, UserAttributesService.Field.ORIGINAL_ID, null ),
userAttributesService.getAttribute( user, UserAttributesService.ATTR_USER_DB_ID, null ),
userAttributesService.getAttribute( user, registrationId, UserAttributesService.Field.LOGIN , null ),
userAttributesService.getAttribute( user, registrationId, UserAttributesService.Field.NAME , null ),
userAttributesService.getAttribute( user, registrationId, UserAttributesService.Field.LOCATION , null ),
userAttributesService.getAttribute( user, registrationId, UserAttributesService.Field.URL , null ),
userAttributesService.getAttribute( user, registrationId, UserAttributesService.Field.AVATAR_URL , null )
);
}

Expand Down
3 changes: 3 additions & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ app.openai-api-url=https://api.openai.com/v1/chat/completions
spring.security.oauth2.client.registration.github.client-id=${OAUTH_GITHUB_CLIENT_ID}
spring.security.oauth2.client.registration.github.client-secret=${OAUTH_GITHUB_CLIENT_SECRET}
spring.security.oauth2.client.registration.github.scope=none
spring.security.oauth2.client.registration.google.client-id=${OAUTH_GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${OAUTH_GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile
app.security.initial-admin=${INITIAL_ADMIN}
Loading

0 comments on commit be32ce1

Please sign in to comment.