diff --git a/pom.xml b/pom.xml
index d17c193..77849c6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,6 +30,27 @@
21
+
+
+
+ io.jsonwebtoken
+ jjwt
+ 0.9.1
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
org.springframework.boot
spring-boot-starter-validation
@@ -138,20 +159,20 @@
3.7.2
-
org.apache.tinkerpop
gremlin-server
3.7.2
-
org.opencypher
util-9.0
9.0.1
-
+
diff --git a/src/main/java/com/stktrk/app/BasicConfiguration.java b/src/main/java/com/stktrk/app/BasicConfiguration.java
deleted file mode 100644
index 514dd99..0000000
--- a/src/main/java/com/stktrk/app/BasicConfiguration.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package com.stktrk.app;
-
-import jakarta.annotation.Nonnull;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.security.config.Customizer;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.core.userdetails.User;
-import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.security.crypto.factory.PasswordEncoderFactories;
-import org.springframework.security.crypto.password.PasswordEncoder;
-import org.springframework.security.provisioning.InMemoryUserDetailsManager;
-import org.springframework.security.web.SecurityFilterChain;
-
-/**
- * Code was copied from here
- */
-@Configuration
-@EnableWebSecurity
-public class BasicConfiguration {
-
- @Bean
- @Nonnull
- public InMemoryUserDetailsManager userDetailsService(@Nonnull PasswordEncoder passwordEncoder) {
- UserDetails user = User.withUsername("Rasmus")
- .password(passwordEncoder.encode("password"))
- .roles("USER")
- .build();
- UserDetails admin = User.withUsername("Christian")
- .password(passwordEncoder.encode("password"))
- .roles("USER", "ADMIN")
- .build();
- return new InMemoryUserDetailsManager(user, admin);
- }
-
- @Bean
- @Nonnull
- public SecurityFilterChain filterChain(@Nonnull HttpSecurity http) throws Exception {
- return http.authorizeHttpRequests(request -> request.anyRequest()
- .authenticated())
- .httpBasic(Customizer.withDefaults())
- .build();
- }
-
- @Bean
- @Nonnull
- public PasswordEncoder passwordEncoder() {
- return PasswordEncoderFactories.createDelegatingPasswordEncoder();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/stktrk/app/application/profile/ProfileController.java b/src/main/java/com/stktrk/app/application/profile/ProfileController.java
deleted file mode 100644
index bab7ba3..0000000
--- a/src/main/java/com/stktrk/app/application/profile/ProfileController.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package com.stktrk.app.application.profile;
-
-import com.stktrk.app.domain.profile.ProfileService;
-import com.stktrk.app.domain.profile.types.Profile;
-import jakarta.annotation.Nonnull;
-import jakarta.validation.Valid;
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
-import org.springframework.security.access.annotation.Secured;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.security.core.Authentication;
-import org.springframework.validation.FieldError;
-import org.springframework.web.bind.MethodArgumentNotValidException;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.security.RolesAllowed;
-import java.security.InvalidKeyException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-@RestController
-@RequestMapping("profile")
-@AllArgsConstructor(access = AccessLevel.PROTECTED)
-public class ProfileController {
-
- @Autowired
- @Nonnull
- private final ProfileService profileService;
-
- // TODO figure out how to handle the exception.
- @Nonnull
- @GetMapping("/")
- public List> findAll(@Nonnull Authentication authentication) {
- return profileService.findAll();
- }
-
- @PostMapping ("/")
- public void addProfile (@Nonnull @Valid @RequestBody Profile profile){
- profileService.addProfile(profile);
- }
-
-
- @GetMapping("/_delete")
- @Nonnull
- public ResponseEntity delete() {
- profileService.deleteAll ();
- return ResponseEntity.status(HttpStatus.OK)
- .body("Deleted all Profiles");
- }
-
- @GetMapping ("/_fake")
- @Nonnull
- public ResponseEntity _fake() {
- profileService.createFakeProfile();
- return ResponseEntity.status(HttpStatus.OK)
- .body("Created Profile");
- }
-
- @ResponseStatus(HttpStatus.BAD_REQUEST)
- @ExceptionHandler(MethodArgumentNotValidException.class)
- @Nonnull
- public Map handleValidationExceptions(
- @Nonnull MethodArgumentNotValidException ex) {
- Map errors = new HashMap<>();
- ex.getBindingResult().getAllErrors().forEach((error) -> {
- String fieldName = ((FieldError) error).getField();
- String errorMessage = error.getDefaultMessage();
- errors.put(fieldName, errorMessage);
- });
- return errors;
- }
-}
diff --git a/src/main/java/com/stktrk/app/domain/profile/ProfileRepository.java b/src/main/java/com/stktrk/app/domain/profile/ProfileRepository.java
deleted file mode 100644
index 16cd786..0000000
--- a/src/main/java/com/stktrk/app/domain/profile/ProfileRepository.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.stktrk.app.domain.profile;
-
-import com.stktrk.app.domain.profile.types.Profile;
-import jakarta.annotation.Nonnull;
-
-import java.util.List;
-
-public interface ProfileRepository {
- @Nonnull
- List> findAll();
-
- void addProfile(@Nonnull Profile profile);
-
- void createFakeProfile();
-
- void deleteAll();
-}
diff --git a/src/main/java/com/stktrk/app/domain/profile/ProfileService.java b/src/main/java/com/stktrk/app/domain/profile/ProfileService.java
deleted file mode 100644
index 23b9f26..0000000
--- a/src/main/java/com/stktrk/app/domain/profile/ProfileService.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.stktrk.app.domain.profile;
-
-import com.stktrk.app.domain.profile.types.Profile;
-import jakarta.annotation.Nonnull;
-import lombok.AllArgsConstructor;
-import org.springframework.stereotype.Service;
-
-import java.util.List;
-
-@Service
-@AllArgsConstructor
-public class ProfileService {
- @Nonnull private final ProfileRepository profileRepository;
-
- @Nonnull
- public List> findAll() {
- return profileRepository.findAll ();
- }
-
- public void addProfile (@Nonnull Profile profile) {
- profileRepository.addProfile (profile);
- }
-
- public void createFakeProfile() {
- profileRepository.createFakeProfile();
- }
-
- public void deleteAll() {
- profileRepository.deleteAll();
- }
-}
diff --git a/src/main/java/com/stktrk/app/domain/profile/types/Profile.java b/src/main/java/com/stktrk/app/domain/profile/types/Profile.java
deleted file mode 100644
index dec3485..0000000
--- a/src/main/java/com/stktrk/app/domain/profile/types/Profile.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.stktrk.app.domain.profile.types;
-
-
-import jakarta.annotation.Nullable;
-import jakarta.validation.constraints.NotEmpty;
-import lombok.Builder;
-import lombok.Data;
-
-@Builder
-@Data
-public class Profile {
-
- @Nullable
- private String id;
- @NotEmpty(message = "Name is mandatory")
- private String name;
- @NotEmpty(message = "Last name is mandatory")
- private String lastName;
-
-}
diff --git a/src/main/java/com/stktrk/app/integration/flyway/migrations/V1__CreateTest.java b/src/main/java/com/stktrk/app/integration/flyway/migrations/V1__CreateTest.java
deleted file mode 100644
index 213e0cf..0000000
--- a/src/main/java/com/stktrk/app/integration/flyway/migrations/V1__CreateTest.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.stktrk.app.integration.flyway.migrations;
-
-import com.stktrk.app.integration.configuration.types.GraphDbConfig;
-import com.stktrk.app.integration.flyway.MigrationWrapper;
-import jakarta.annotation.Nonnull;
-import lombok.EqualsAndHashCode;
-import lombok.Value;
-import org.flywaydb.core.api.migration.BaseJavaMigration;
-import org.flywaydb.core.api.migration.Context;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-@EqualsAndHashCode(callSuper = true)
-@Component
-@Value
-public class V1__CreateTest extends BaseJavaMigration implements MigrationWrapper {
- @Autowired
- @Nonnull
- GraphDbConfig graphDbConfig;
-
- public void migrate(@Nonnull Context context) throws Exception {
-
- this.executeQuery(graphDbConfig,"CREATE VERTEX TYPE test");
- }
-}
diff --git a/src/main/java/com/stktrk/app/integration/flyway/migrations/V2__CreateUser.java b/src/main/java/com/stktrk/app/integration/flyway/migrations/V2__CreateUser.java
deleted file mode 100644
index 23b034b..0000000
--- a/src/main/java/com/stktrk/app/integration/flyway/migrations/V2__CreateUser.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.stktrk.app.integration.flyway.migrations;
-
-import com.stktrk.app.integration.configuration.types.GraphDbConfig;
-import com.stktrk.app.integration.flyway.MigrationWrapper;
-import jakarta.annotation.Nonnull;
-import lombok.EqualsAndHashCode;
-import lombok.Value;
-import org.flywaydb.core.api.migration.BaseJavaMigration;
-import org.flywaydb.core.api.migration.Context;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-@EqualsAndHashCode(callSuper = true)
-@Component
-@Value
-public class V2__CreateUser extends BaseJavaMigration implements MigrationWrapper {
- @Autowired
- @Nonnull
- GraphDbConfig graphDbConfig;
-
- public void migrate(@Nonnull Context context) throws Exception {
-
- this.executeQuery(graphDbConfig,"CREATE VERTEX TYPE user");
- }
-}
diff --git a/src/main/java/com/stktrk/app/integration/profile/ProfileRepositoryImpl.java b/src/main/java/com/stktrk/app/integration/profile/ProfileRepositoryImpl.java
deleted file mode 100644
index 508a93a..0000000
--- a/src/main/java/com/stktrk/app/integration/profile/ProfileRepositoryImpl.java
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.stktrk.app.integration.profile;
-
-import com.arcadedb.gremlin.ArcadeGraph;
-import com.github.javafaker.Faker;
-import com.stktrk.app.integration.flyway.ConnectionPool;
-import com.stktrk.app.domain.profile.ProfileRepository;
-import com.stktrk.app.domain.profile.types.Profile;
-import com.stktrk.app.utils.EncryptId;
-import jakarta.annotation.Nonnull;
-import org.apache.tinkerpop.gremlin.structure.Vertex;
-import org.springframework.stereotype.Repository;
-
-import java.util.List;
-import java.util.Random;
-
-@Repository
-public class ProfileRepositoryImpl implements ProfileRepository {
- @Nonnull
- @Override
- public List> findAll() {
- ArcadeGraph g = ConnectionPool.getGraph();
- List extends Vertex> profiles = g.traversal().V().hasLabel("Profile").toList();
- g.close();
-
- return profiles.stream()
- .map(this::buildProfile)
- .toList();
- }
-
- @Override
- public void addProfile(@Nonnull Profile profile) {
- ArcadeGraph g = ConnectionPool.getGraph();
- Vertex profiles = g.traversal()
- .addV("Profile")
- .property("name", profile.getName())
- .property("lastName", profile.getLastName())
- .next();
- g.close();
- }
-
- @Override
- public void createFakeProfile() {
- Faker faker = new Faker();
- ArcadeGraph g = ConnectionPool.getGraph();
- List vertices = g.traversal().V().hasLabel("Profile").toList();
- Vertex target = vertices.isEmpty() ? null : vertices.get(new Random().nextInt(vertices.size()));
-
- Vertex res = g.traversal().addV("Profile").property("name", faker.name().firstName()).property("lastName", faker.name().lastName())
- .next();
- if (target != null) {
- g.traversal().V(res.id()).addE("friend").to(target).iterate();
- }
- g.close();
- }
-
- @Override
- public void deleteAll () {
- ArcadeGraph g = ConnectionPool.getGraph();
- g.traversal().V().hasLabel("Profile").drop().iterate();
- }
- @Nonnull
- private Profile buildProfile (@Nonnull Vertex v) {
- return Profile.builder()
- .name((String) v.property("name").value())
- .lastName((String) v.property("lastName").value())
- .id(EncryptId.encrypt ((String)v.id()))
- .build();
- }
-
-}
diff --git a/src/main/java/com/stktrk/app/AppApplication.java b/src/main/java/com/stktrk/auth/AppApplication.java
similarity index 96%
rename from src/main/java/com/stktrk/app/AppApplication.java
rename to src/main/java/com/stktrk/auth/AppApplication.java
index 3265a3e..bf06477 100644
--- a/src/main/java/com/stktrk/app/AppApplication.java
+++ b/src/main/java/com/stktrk/auth/AppApplication.java
@@ -1,4 +1,4 @@
-package com.stktrk.app;
+package com.stktrk.auth;
import jakarta.annotation.Nullable;
import org.springframework.boot.SpringApplication;
diff --git a/src/main/java/com/stktrk/auth/CustomGrantedAuthority.java b/src/main/java/com/stktrk/auth/CustomGrantedAuthority.java
new file mode 100644
index 0000000..98be48d
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/CustomGrantedAuthority.java
@@ -0,0 +1,17 @@
+package com.stktrk.auth;
+
+import org.springframework.security.core.GrantedAuthority;
+
+public class CustomGrantedAuthority implements GrantedAuthority {
+
+ private final String role;
+
+ public CustomGrantedAuthority(String role) {
+ this.role = role;
+ }
+
+ @Override
+ public String getAuthority() {
+ return "ROLE_" + role.toUpperCase(); // Converts to Spring-standard role format
+ }
+}
diff --git a/src/main/java/com/stktrk/auth/CustomOAuth2UserService.java b/src/main/java/com/stktrk/auth/CustomOAuth2UserService.java
new file mode 100644
index 0000000..9412933
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/CustomOAuth2UserService.java
@@ -0,0 +1,54 @@
+package com.stktrk.auth;
+
+import com.stktrk.auth.domain.account.AccountService;
+import com.stktrk.auth.domain.account.types.Account;
+import com.stktrk.auth.domain.account.types.Role;
+import jakarta.annotation.Nonnull;
+import lombok.SneakyThrows;
+import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.stereotype.Service;
+
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class CustomOAuth2UserService extends DefaultOAuth2UserService {
+
+ @Nonnull
+ private final AccountService accountService;
+
+ @Nonnull public CustomOAuth2UserService(@Nonnull AccountService accountService) {
+ this.accountService = accountService;
+ }
+
+
+ @Override
+ @Nonnull public OAuth2User loadUser(@Nonnull OAuth2UserRequest userRequest) {
+ OAuth2User oAuth2User = super.loadUser(userRequest);
+
+ Map attributes = oAuth2User.getAttributes();
+ String email = (String) attributes.get("email");
+
+ // Check if user exists, else create a new user
+ Account existingAccount = accountService.findByEmail(email);
+ if (existingAccount == null) {
+ try {
+ accountService.registerUser(email, "generated-password", List.of(Role.USER));
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return new DefaultOAuth2User(
+ Collections.singleton(() -> "ROLE_USER"),
+ attributes,
+ "id"
+ );
+ }
+}
+
diff --git a/src/main/java/com/stktrk/auth/JwtConfig.java b/src/main/java/com/stktrk/auth/JwtConfig.java
new file mode 100644
index 0000000..4887f7a
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/JwtConfig.java
@@ -0,0 +1,47 @@
+package com.stktrk.auth;
+
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+
+@Configuration
+public class JwtConfig {
+
+ @Bean
+ public KeyPair keyPair() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ return keyPairGenerator.generateKeyPair();
+ }
+
+ @Bean
+ public JWKSource jwkSource(KeyPair keyPair) {
+ RSAKey rsaKey = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
+ .privateKey((RSAPrivateKey) keyPair.getPrivate())
+ .keyID("my-key-id")
+ .build();
+ return new ImmutableJWKSet<>(new com.nimbusds.jose.jwk.JWKSet(rsaKey));
+ }
+
+ @Bean
+ public JwtEncoder jwtEncoder(JWKSource jwkSource) {
+ return new NimbusJwtEncoder(jwkSource);
+ }
+
+ @Bean
+ public JwtDecoder jwtDecoder(KeyPair keyPair) {
+ return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
+ }
+}
diff --git a/src/main/java/com/stktrk/auth/RBACConfig.java b/src/main/java/com/stktrk/auth/RBACConfig.java
new file mode 100644
index 0000000..1bcdea6
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/RBACConfig.java
@@ -0,0 +1,18 @@
+package com.stktrk.auth;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Service;
+
+@Service
+public class RBACConfig {
+
+ @PreAuthorize("hasRole('ADMIN')")
+ public String adminOnlyOperation() {
+ return "Admin operation success!";
+ }
+
+ @PreAuthorize("hasRole('USER')")
+ public String userOperation() {
+ return "User operation success!";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/stktrk/auth/RoleAuthentication.java b/src/main/java/com/stktrk/auth/RoleAuthentication.java
new file mode 100644
index 0000000..25eb88d
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/RoleAuthentication.java
@@ -0,0 +1,64 @@
+package com.stktrk.auth;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+import javax.security.auth.Subject;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+class RoleAuthentication implements Authentication {
+ private final List roles;
+ private boolean authenticated = false;
+
+ public RoleAuthentication(List roles) {
+ this.roles = roles;
+ }
+
+
+ @Override
+ public String getName() {
+ return "roles";
+ }
+
+ @Override
+ public boolean implies(Subject subject) {
+ return Authentication.super.implies(subject);
+ }
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return roles.stream()
+ .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public Object getCredentials() {
+ return null;
+ }
+
+ @Override
+ public Object getDetails() {
+ return null;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return null;
+ }
+
+ @Override
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ @Override
+ public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
+ this.authenticated = isAuthenticated;
+ }
+
+ // Implement other methods as required
+}
\ No newline at end of file
diff --git a/src/main/java/com/stktrk/auth/SecurityConfig.java b/src/main/java/com/stktrk/auth/SecurityConfig.java
new file mode 100644
index 0000000..d9a637e
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/SecurityConfig.java
@@ -0,0 +1,86 @@
+package com.stktrk.auth;
+
+// **SecurityConfig.java**
+import jakarta.annotation.Nonnull;
+import lombok.AllArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity(prePostEnabled = true)
+@AllArgsConstructor
+public class SecurityConfig {
+
+ @Autowired
+ @Nonnull
+ private final JwtDecoder jwtDecoder;
+
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http
+ .authorizeHttpRequests(auth -> auth
+ .requestMatchers("/public/**", "/auth/**", "/test/signup").permitAll()
+ .requestMatchers("/v3/api-docs/**", "/swagger-ui/**").permitAll()
+ //.requestMatchers("/bearer/**").authenticated() ANNOTATION SEEMS TO WORK?
+ .anyRequest().authenticated()
+ )
+
+ .addFilterBefore(new TokenValidatorFilter(jwtDecoder), UsernamePasswordAuthenticationFilter.class)
+
+ .oauth2Login(oauth2 -> oauth2
+ .defaultSuccessUrl("/auth/success")
+ .failureUrl("/auth/failure")
+ )
+ // FiXME !!!! this is a security risk.
+
+ .sessionManagement(session -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ )
+ .csrf(csrf -> csrf.disable());
+
+
+ return http.build();
+ }
+
+ @Bean
+ public ClientRegistrationRepository clientRegistrationRepository() {
+ ClientRegistration facebookRegistration = ClientRegistration.withRegistrationId("facebook")
+ .clientId("your-facebook-client-id")
+ .clientSecret("your-facebook-client-secret")
+ .redirectUri("{baseUrl}/auth/login/oauth2/code/facebook")
+ .authorizationUri("https://www.facebook.com/v11.0/dialog/oauth")
+ .tokenUri("https://graph.facebook.com/v11.0/oauth/access_token")
+ .userInfoUri("https://graph.facebook.com/me?fields=id,name,email")
+ .userNameAttributeName("id")
+ .clientName("Facebook")
+ .scope("email", "public_profile")
+ .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+ .build();
+
+ return new InMemoryClientRegistrationRepository(facebookRegistration);
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/stktrk/auth/TokenValidatorFilter.java b/src/main/java/com/stktrk/auth/TokenValidatorFilter.java
new file mode 100644
index 0000000..a01eb9c
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/TokenValidatorFilter.java
@@ -0,0 +1,63 @@
+package com.stktrk.auth;
+
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.AllArgsConstructor;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.util.Collections;
+
+import java.io.IOException;
+import java.util.List;
+
+@AllArgsConstructor
+public class TokenValidatorFilter extends OncePerRequestFilter {
+
+ private final JwtDecoder jwtDecoder;
+
+
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+
+ String authHeader = request.getHeader("Authorization");
+
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ String token = authHeader.substring(7);
+ try {
+ // Decode and validate the JWT token
+ Jwt jwt = jwtDecoder.decode(token);
+
+ // Extract claims (e.g., role and username)
+ String username = jwt.getSubject();
+ List roles = jwt.getClaim("role"); // Assumes your JWT has a 'role' claim
+
+ // Set up Spring Security's authentication
+ if (roles != null && !roles.isEmpty()) {
+ RoleAuthentication roleAuth = new RoleAuthentication(roles);
+ roleAuth.setAuthenticated(true);
+ SecurityContextHolder.getContext()
+ .setAuthentication(roleAuth);
+ }
+
+
+ } catch (Exception e) {
+ // Log error and send unauthorized response
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT");
+ return;
+ }
+ }
+
+ // Proceed with the filter chain
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/com/stktrk/auth/application/AuthController.java b/src/main/java/com/stktrk/auth/application/AuthController.java
new file mode 100644
index 0000000..b4f53cb
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/application/AuthController.java
@@ -0,0 +1,114 @@
+package com.stktrk.auth.application;
+
+import com.stktrk.auth.domain.account.AccountService;
+import com.stktrk.auth.domain.account.types.Account;
+import jakarta.annotation.Nonnull;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.oauth2.jwt.JwtClaimsSet;
+import org.springframework.security.oauth2.jwt.JwtEncoder;
+import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.security.KeyPair;
+import java.time.Instant;
+import java.util.HexFormat;
+
+@RestController
+@RequestMapping("/auth")
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public class AuthController {
+ @Nonnull
+ @Autowired
+ private final AccountService accountService;
+
+ @Autowired
+ @Nonnull
+ private final JwtEncoder jwtEncoder;
+
+ @Autowired
+ @Nonnull
+ private final KeyPair keyPair;
+
+
+ /**
+ * @return success notification for social login
+ */
+ @Nonnull
+ @GetMapping("/success")
+ public String success() {
+ return "Login successful!";
+ }
+
+ /**
+ * @return failure notification for social login
+ */
+ @Nonnull
+ @GetMapping("/failure")
+ public String failure() {
+ return "Login failed.";
+ }
+
+
+ // FIXME this creates valid tokens on the fly, should be removed from production, or protected for local and dev use only
+ @PostMapping("/token")
+ public String generateToken(@RequestParam String username, @RequestParam String role) {
+ JwtClaimsSet claims = JwtClaimsSet.builder()
+ .issuer("auth-service")
+ .issuedAt(Instant.now())
+ .expiresAt(Instant.now().plusSeconds(3600))
+ .subject(username)
+ .claim("role", role)
+ .build();
+
+ return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
+ }
+
+ /**
+ *
+ * Login to local account
+ *
+ * @param username usanername
+ * @param password password
+ * @return JWT token
+ */
+ @Nonnull
+ @PostMapping("/login")
+ public String login(@RequestParam String username, @RequestParam String password) {
+ Account account = accountService.findByEmail(username);
+ if (account == null || !accountService.verifyPassword(password, account.getPassword())) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials");
+ }
+
+
+ // Generate JWT
+ JwtClaimsSet claims = JwtClaimsSet.builder()
+ .issuer("auth-service")
+ .issuedAt(Instant.now())
+ .expiresAt(Instant.now().plusSeconds(3600))
+ .subject(username)
+ .claim("role", account.getRoles().stream().map ( r-> r.name()).toList())
+ .build();
+
+ return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
+ }
+ // check result in https://fusionauth.io/dev-tools/jwt-decoder
+
+ /**
+ * Offers the possibility for other severs to get to the public key
+ * FIXME should probably not be a thing in production?
+ * @return public key used by the server
+ */
+ @GetMapping("/publicKey")
+ public String publicKey() {
+
+
+ return "{\"public\":\"" + HexFormat.of().formatHex(keyPair.getPublic().getEncoded()) + "\"}";
+
+ }
+
+
+}
diff --git a/src/main/java/com/stktrk/auth/application/LocalSignupController.java b/src/main/java/com/stktrk/auth/application/LocalSignupController.java
new file mode 100644
index 0000000..fe13efb
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/application/LocalSignupController.java
@@ -0,0 +1,41 @@
+package com.stktrk.auth.application;
+
+import com.stktrk.auth.domain.account.types.Role;
+import com.stktrk.auth.domain.account.AccountService;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.sql.SQLException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/test")
+public class LocalSignupController {
+
+ @Nullable
+ private final AccountService accountService;
+
+ public LocalSignupController(@Nullable AccountService accountService) {
+ this.accountService = accountService;
+ }
+
+
+ @Nonnull
+ @PostMapping("/signup")
+ public String signUp(@Nonnull @RequestParam String email, @Nonnull @RequestParam String password, @Nonnull @RequestParam List roles) {
+
+
+ // Register the user
+ try {
+ accountService.registerUser(email, password, roles);
+ } catch (SQLException e) {
+ return e.getMessage();
+ }
+ return "User registered successfully!";
+ }
+
+}
diff --git a/src/main/java/com/stktrk/auth/application/TestController.java b/src/main/java/com/stktrk/auth/application/TestController.java
new file mode 100644
index 0000000..d305ea7
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/application/TestController.java
@@ -0,0 +1,20 @@
+package com.stktrk.auth.application;
+
+import jakarta.annotation.Nonnull;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/bearer")
+public class TestController {
+
+ @PreAuthorize("hasRole('USER')")
+@Nonnull
+ @GetMapping ("/user")
+ public String isUser () {
+ return "OK";
+}
+
+}
diff --git a/src/main/java/com/stktrk/auth/domain/account/AccountRepository.java b/src/main/java/com/stktrk/auth/domain/account/AccountRepository.java
new file mode 100644
index 0000000..b144172
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/domain/account/AccountRepository.java
@@ -0,0 +1,36 @@
+package com.stktrk.auth.domain.account;
+
+import com.stktrk.auth.domain.account.types.Role;
+import com.stktrk.auth.domain.account.types.Account;
+import jakarta.annotation.Nonnull;
+
+import java.util.List;
+/**
+ * Profile repository
+ */
+public interface AccountRepository {
+ /**
+ * Check if a user profile exists, by email
+ * @param email account email
+ * @return account exits
+ */
+ boolean accountExists(@Nonnull String email);
+
+ /**
+ * returns a single account, with roles
+ * @param email account email
+ * @return account, incl Roles.
+ */
+ @Nonnull
+ Account findByEmail (@Nonnull String email);
+
+ /**
+ * Create a new user account
+ *
+ * @param email account email serves as unique id
+ * @param password password.
+ * @param roles roles the account is created with.
+ */
+ void createAccount(@Nonnull String email, @Nonnull String password, List roles);
+
+}
diff --git a/src/main/java/com/stktrk/auth/domain/account/AccountService.java b/src/main/java/com/stktrk/auth/domain/account/AccountService.java
new file mode 100644
index 0000000..4c7aee8
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/domain/account/AccountService.java
@@ -0,0 +1,61 @@
+package com.stktrk.auth.domain.account;
+
+import com.stktrk.auth.domain.account.types.Role;
+import com.stktrk.auth.domain.account.types.Account;
+import jakarta.annotation.Nonnull;
+import lombok.AllArgsConstructor;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * account service
+ */
+@Service
+@AllArgsConstructor
+public class AccountService {
+ @Nonnull
+ private final AccountRepository accountRepository;
+ @Nonnull
+ private final PasswordEncoder passwordEncoder;
+
+ /**
+ * register a new user account
+ * @param email account email, serves as uniwue ID
+ * @param password password
+ * @param roles roles to be assigned to the account
+ * @throws SQLException thrown on duplicate account creation FIXME
+ */
+ public void registerUser(@Nonnull String email, @Nonnull String password,@Nonnull List roles) throws SQLException {
+
+ String encodedPassword = passwordEncoder.encode(password);
+ if (accountRepository.accountExists(email)) {
+ throw new SQLException("User already exists.");
+ }
+ accountRepository.createAccount (email,encodedPassword, roles);
+ }
+
+ /**
+ * used during login to check password validity FIXME we should not expose the password to anywhere should we?
+ * @param rawPassword
+ * @param encodedPassword
+ * @return
+ */
+ public boolean verifyPassword(@Nonnull String rawPassword, @Nonnull String encodedPassword) {
+ return passwordEncoder.matches(rawPassword, encodedPassword);
+ }
+
+ /**
+ * search account by email
+ * @param email account email
+ * @return account
+ */
+ public Account findByEmail (@Nonnull String email) {
+ return accountRepository.findByEmail (email);
+ }
+}
+
+
+
diff --git a/src/main/java/com/stktrk/auth/domain/account/types/Account.java b/src/main/java/com/stktrk/auth/domain/account/types/Account.java
new file mode 100644
index 0000000..39046df
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/domain/account/types/Account.java
@@ -0,0 +1,19 @@
+package com.stktrk.auth.domain.account.types;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import lombok.Builder;
+import lombok.Value;
+
+import java.util.List;
+
+@Value
+@Builder
+public class Account {
+ @Nonnull
+ private final String email;
+ @Nullable
+ private final String password;
+ @Nullable private final List roles;
+ @Nullable private final String id;
+}
diff --git a/src/main/java/com/stktrk/auth/domain/account/types/Role.java b/src/main/java/com/stktrk/auth/domain/account/types/Role.java
new file mode 100644
index 0000000..71cfbc3
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/domain/account/types/Role.java
@@ -0,0 +1,6 @@
+package com.stktrk.auth.domain.account.types;
+
+public enum Role {
+ ADMIN,
+ USER
+}
diff --git a/src/main/java/com/stktrk/auth/integration/account/AccountRepositoryImpl.java b/src/main/java/com/stktrk/auth/integration/account/AccountRepositoryImpl.java
new file mode 100644
index 0000000..2a549ce
--- /dev/null
+++ b/src/main/java/com/stktrk/auth/integration/account/AccountRepositoryImpl.java
@@ -0,0 +1,111 @@
+package com.stktrk.auth.integration.account;
+
+import com.arcadedb.gremlin.ArcadeGraph;
+import com.stktrk.auth.domain.account.types.Role;
+import com.stktrk.auth.domain.account.AccountRepository;
+import com.stktrk.auth.domain.account.types.Account;
+import com.stktrk.auth.integration.flyway.ConnectionPool;
+import com.stktrk.auth.utils.EncryptId;
+import jakarta.annotation.Nonnull;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.StreamSupport;
+
+import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.outE;
+
+/**
+ * Profile repository
+ */
+@Repository
+public class AccountRepositoryImpl implements AccountRepository {
+ /**
+ * Check if a user profile exists, by email
+ * @param email account email
+ * @return account exits
+ */
+ @Override
+ public boolean accountExists(@Nonnull String email) {
+ ArcadeGraph g = ConnectionPool.getGraph();
+ List extends Vertex> accounts = g.traversal().V().hasLabel("Account").has("email", email).toList();
+ g.close();
+
+ return !accounts.isEmpty();
+ }
+
+ /**
+ * returns a single account, with roles
+ * @param email account email
+ * @return account, incl Roles.
+ */
+ @Override
+ public Account findByEmail(@Nonnull String email) {
+ ArcadeGraph g = ConnectionPool.getGraph();
+ List