commit 872850da8988801719ade02344f610e04d458bf3 Author: john Date: Mon Nov 24 16:42:06 2025 -0700 chore: restore from backup project was archived and never imported into my new instance of gittea, until now diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a4f240 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# identity-management + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fedab2e --- /dev/null +++ b/pom.xml @@ -0,0 +1,110 @@ + + + 4.0.0 + + com.jkgroller + identity-management + 1.0.0-SNAPSHOT + jar + + IdentityManagement + Authentication with JSONAPI. + + + org.springframework.boot + spring-boot-starter-parent + 2.2.6.RELEASE + + + + UTF-8 + UTF-8 + 8 + 3.5 + 3.2.20200419165537 + 2.5 + 1.3.1.Final + 1.14 + + + + + jcenter + https://jcenter.bintray.com/ + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-devtools + + + com.h2database + h2 + + + commons-codec + commons-codec + ${commons-codec.version} + + + commons-io + commons-io + ${commons.io.version} + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + + + io.crnk + crnk-setup-spring-boot2 + ${crnk.version} + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + + IdentityManagement + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + + + + diff --git a/src/main/java/com/jkgroller/identitymanagement/IdentityManagement.java b/src/main/java/com/jkgroller/identitymanagement/IdentityManagement.java new file mode 100644 index 0000000..ffbcf94 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/IdentityManagement.java @@ -0,0 +1,14 @@ +package com.jkgroller.identitymanagement; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class IdentityManagement { + + public static void main(String[] args) { + SpringApplication.run(IdentityManagement.class, args); + } + +} + diff --git a/src/main/java/com/jkgroller/identitymanagement/config/KeyGeneratorConfig.java b/src/main/java/com/jkgroller/identitymanagement/config/KeyGeneratorConfig.java new file mode 100644 index 0000000..ec9d4b5 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/config/KeyGeneratorConfig.java @@ -0,0 +1,38 @@ +package com.jkgroller.identitymanagement.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.keygen.KeyGenerators; + +@Configuration +public class KeyGeneratorConfig { + + private final int IDENTIFIER_LENGTH; + + private final int TOKEN_LENGTH; + + /** + * This is how you inject a value from a properties file into a constant. + */ + public KeyGeneratorConfig(@Value("${identifier.length}") int identifierLength, @Value("${token.length}") int tokenLength) { + this.IDENTIFIER_LENGTH = identifierLength; + this.TOKEN_LENGTH = tokenLength; + } + + /** + * @return + */ + @Bean(name = "identifierGenerator") + public BytesKeyGenerator identifierGenerator() { + return KeyGenerators.secureRandom(IDENTIFIER_LENGTH); + } + + @Bean(name = "tokenGenerator") + public BytesKeyGenerator tokenGenerator() { + return KeyGenerators.secureRandom(TOKEN_LENGTH); + } + + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/config/SecurityConfig.java b/src/main/java/com/jkgroller/identitymanagement/config/SecurityConfig.java new file mode 100644 index 0000000..0383d47 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/config/SecurityConfig.java @@ -0,0 +1,42 @@ +package com.jkgroller.identitymanagement.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + //... + } + + /** + * + * @param httpSecurity + * @throws Exception + */ + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + //... + } + + /** + * + * @param web + * @throws Exception + */ + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/h2/**", "/v1/**"); + } + + +} + diff --git a/src/main/java/com/jkgroller/identitymanagement/converter/CycleAvoidingMappingContext.java b/src/main/java/com/jkgroller/identitymanagement/converter/CycleAvoidingMappingContext.java new file mode 100644 index 0000000..bc57f12 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/converter/CycleAvoidingMappingContext.java @@ -0,0 +1,24 @@ +package com.jkgroller.identitymanagement.converter; + +import org.mapstruct.BeforeMapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.TargetType; +import org.springframework.stereotype.Component; + +import java.util.IdentityHashMap; +import java.util.Map; + +@Component +public class CycleAvoidingMappingContext { + private Map knownInstances = new IdentityHashMap(); + + @BeforeMapping + public T getMappedInstance(Object source, @TargetType Class targetType) { + return (T) knownInstances.get( source ); + } + + @BeforeMapping + public void storeMappedInstance(Object source, @MappingTarget Object target) { + knownInstances.put( source, target ); + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/converter/IdentityManagementConverter.java b/src/main/java/com/jkgroller/identitymanagement/converter/IdentityManagementConverter.java new file mode 100644 index 0000000..92765e1 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/converter/IdentityManagementConverter.java @@ -0,0 +1,54 @@ +package com.jkgroller.identitymanagement.converter; + +import com.jkgroller.identitymanagement.entity.AuthenticatedSessionEntity; +import com.jkgroller.identitymanagement.entity.IndividualCustomerEntity; +import com.jkgroller.identitymanagement.entity.OnlineAccountEntity; +import com.jkgroller.identitymanagement.meta.OnlineAccountMeta; +import com.jkgroller.identitymanagement.resource.AuthenticatedSessionResource; +import com.jkgroller.identitymanagement.resource.IndividualCustomerResource; +import com.jkgroller.identitymanagement.resource.OnlineAccountResource; +import org.mapstruct.*; + +import java.util.List; + +@Mapper(componentModel = "spring") +public interface IdentityManagementConverter { + + @Mapping(target = "id", source = "identifier") + @Mapping(target = "dbId", source = "id") + AuthenticatedSessionResource authenticatedSessionEntityToAuthenticatedSessionResource(AuthenticatedSessionEntity authenticatedSessionEntity, @Context CycleAvoidingMappingContext context); + List authenticatedSessionEntitiesToAuthenticatedSessionResources(List authenticatedSessionEntities, @Context CycleAvoidingMappingContext context); + + @Mapping(target = "identifier", source = "id") + @Mapping(target = "id", source = "dbId") + AuthenticatedSessionEntity authenticatedSessionResourceToAuthenticatedSessionEntity(AuthenticatedSessionResource authenticatedSessionResource, @Context CycleAvoidingMappingContext context); + + @Mapping(target = "id", source = "identifier") + @Mapping(target = "dbId", source = "id") + IndividualCustomerResource individualCustomerEntityToIndividualCustomerResource(IndividualCustomerEntity individualCustomerEntity, @Context CycleAvoidingMappingContext context); + List individualCustomerEntitiesToIndividualCustomerResources(List individualCustomerEntities, @Context CycleAvoidingMappingContext context); + + @Mapping(target = "identifier", source = "id") + @Mapping(target = "id", source = "dbId") + IndividualCustomerEntity individualCustomerResourceToIndividualCustomerEntity(IndividualCustomerResource individualCustomerResource, @Context CycleAvoidingMappingContext context); + + @Mapping(target = "identifier", source = "id") + @Mapping(target = "id", source = "dbId") + @Mapping(target = "accountStatus", source = "onlineAccountMeta.accountStatus") + @Mapping(target = "passwordStatus", source = "onlineAccountMeta.passwordStatus") + OnlineAccountEntity onlineAccountResourceToOnlineAccountEntity(OnlineAccountResource onlineAccountResource, @Context CycleAvoidingMappingContext context); + + @Mapping(target = "id", source = "identifier") + @Mapping(target = "dbId", source = "id") + OnlineAccountResource onlineAccountEntityToOnlineAccountResource(OnlineAccountEntity onlineAccountEntity, @Context CycleAvoidingMappingContext context); + List onlineAccountEntitiesToOnlineAccountResources(List onlineAccountEntities, @Context CycleAvoidingMappingContext context); + + @AfterMapping + default void setOnlineAccountResourceMeta(OnlineAccountEntity onlineAccountEntity, @MappingTarget OnlineAccountResource onlineAccountResource) { + OnlineAccountMeta onlineAccountMeta = new OnlineAccountMeta(); + onlineAccountMeta.setAccountStatus(onlineAccountEntity.getAccountStatus()); + onlineAccountMeta.setPasswordStatus(onlineAccountEntity.getPasswordStatus()); + onlineAccountResource.setOnlineAccountMeta(onlineAccountMeta); + } + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/entity/AuthenticatedSessionEntity.java b/src/main/java/com/jkgroller/identitymanagement/entity/AuthenticatedSessionEntity.java new file mode 100644 index 0000000..9fe5392 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/entity/AuthenticatedSessionEntity.java @@ -0,0 +1,94 @@ +package com.jkgroller.identitymanagement.entity; + +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; + +import javax.persistence.*; + +@Entity(name = "authenticated_session") +public class AuthenticatedSessionEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String tokenType; // Type of token/session + + private String identifier; // Universally unique identifier + + private String token; // The token + + private String userName; + + private String expiresIn; + + private String levelOfAssurance; + + @OneToOne + @LazyCollection(LazyCollectionOption.FALSE) + private OnlineAccountEntity onlineAccount; + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(String expiresIn) { + this.expiresIn = expiresIn; + } + + public String getLevelOfAssurance() { + return levelOfAssurance; + } + + public void setLevelOfAssurance(String levelOfAssurance) { + this.levelOfAssurance = levelOfAssurance; + } + + public OnlineAccountEntity getOnlineAccount() { + return onlineAccount; + } + + public void setOnlineAccount(OnlineAccountEntity onlineAccount) { + this.onlineAccount = onlineAccount; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/entity/IndividualCustomerEntity.java b/src/main/java/com/jkgroller/identitymanagement/entity/IndividualCustomerEntity.java new file mode 100644 index 0000000..aa96477 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/entity/IndividualCustomerEntity.java @@ -0,0 +1,79 @@ +package com.jkgroller.identitymanagement.entity; + +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; + +import javax.persistence.*; + +@Entity +@Table(name="individual_customer") +public class IndividualCustomerEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + private String firstName; + + @Column + private String lastName; + + @Column + private String identifier; + + @Column + private String emailAddress; + + @OneToOne(mappedBy = "individualCustomer") + @LazyCollection(LazyCollectionOption.FALSE) + private OnlineAccountEntity onlineAccount; + + public OnlineAccountEntity getOnlineAccount() { + return onlineAccount; + } + + public void setOnlineAccount(OnlineAccountEntity onlineAccount) { + this.onlineAccount = onlineAccount; + } + + 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 getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/entity/OnlineAccountEntity.java b/src/main/java/com/jkgroller/identitymanagement/entity/OnlineAccountEntity.java new file mode 100644 index 0000000..0714e37 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/entity/OnlineAccountEntity.java @@ -0,0 +1,144 @@ +package com.jkgroller.identitymanagement.entity; + +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; + +import javax.persistence.*; + +@Entity +@Table(name="online_account") +public class OnlineAccountEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + private String identifier; + + @Column + private String creationDate; + + @Column + private String lastLoginDate; + + @Column + private String accountStatus; + + @Column + private String passwordStatus; + + @Column(unique = true) + private String userName; + + @Column + private String password; + + @Column + private String oneTimePassword; + + @OneToOne + @LazyCollection(LazyCollectionOption.FALSE) + private IndividualCustomerEntity individualCustomer; + + @OneToOne(mappedBy = "onlineAccount") + @LazyCollection(LazyCollectionOption.FALSE) + private AuthenticatedSessionEntity authenticatedSession; + + public String getOneTimePassword() { + return oneTimePassword; + } + + public void setOneTimePassword(String oneTimePassword) { + this.oneTimePassword = oneTimePassword; + } + + public AuthenticatedSessionEntity getAuthenticatedSession() { + return authenticatedSession; + } + + public void setAuthenticatedSession(AuthenticatedSessionEntity authenticatedSession) { + this.authenticatedSession = authenticatedSession; + } + + + public IndividualCustomerEntity getIndividualCustomer() { + return individualCustomer; + } + + public void setIndividualCustomer(IndividualCustomerEntity individualCustomer) { + this.individualCustomer = individualCustomer; + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public IndividualCustomerEntity getIndividualCustomerEntity() { + return individualCustomer; + } + + public void setIndividualCustomerEntity(IndividualCustomerEntity individualCustomerEntity) { + this.individualCustomer = individualCustomerEntity; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCreationDate() { + return creationDate; + } + + public String getAccountStatus() { + return accountStatus; + } + + public void setAccountStatus(String accountStatus) { + this.accountStatus = accountStatus; + } + + public String getPasswordStatus() { + return passwordStatus; + } + + public void setPasswordStatus(String passwordStatus) { + this.passwordStatus = passwordStatus; + } + + public void setCreationDate(String creationDate) { + this.creationDate = creationDate; + } + + public String getLastLoginDate() { + return lastLoginDate; + } + + public void setLastLoginDate(String lastLoginDate) { + this.lastLoginDate = lastLoginDate; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/exception/CreateOnlineAccountConflictException.java b/src/main/java/com/jkgroller/identitymanagement/exception/CreateOnlineAccountConflictException.java new file mode 100644 index 0000000..fe70716 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/exception/CreateOnlineAccountConflictException.java @@ -0,0 +1,24 @@ +package com.jkgroller.identitymanagement.exception; + +import io.crnk.core.engine.document.ErrorData; +import io.crnk.core.engine.http.HttpStatus; +import io.crnk.core.exception.CrnkMappableException; + +public class CreateOnlineAccountConflictException extends CrnkMappableException { + + private static final long serialVersionUID = 1L; + + /** + * Normally we'd use 409 for conflicts, but that tells a hacker that an account exists. + */ + public CreateOnlineAccountConflictException() { + super(HttpStatus.BAD_REQUEST_400, createErrorData()); + } + + private static ErrorData createErrorData() { + return ErrorData.builder().setStatus(String.valueOf(HttpStatus.BAD_REQUEST_400)) + .setDetail("Unable to create online account.").build(); + } + +} + diff --git a/src/main/java/com/jkgroller/identitymanagement/meta/OnlineAccountMeta.java b/src/main/java/com/jkgroller/identitymanagement/meta/OnlineAccountMeta.java new file mode 100644 index 0000000..9a86f77 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/meta/OnlineAccountMeta.java @@ -0,0 +1,26 @@ +package com.jkgroller.identitymanagement.meta; + +import io.crnk.core.resource.meta.MetaInformation; + +public class OnlineAccountMeta implements MetaInformation { + + private String accountStatus; + + private String passwordStatus; + + public String getAccountStatus() { + return accountStatus; + } + + public void setAccountStatus(String accountStatus) { + this.accountStatus = accountStatus; + } + + public String getPasswordStatus() { + return passwordStatus; + } + + public void setPasswordStatus(String passwordStatus) { + this.passwordStatus = passwordStatus; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/relationship/repository/AuthenticatedSessionToOnlineAccountRelationshipRepository.java b/src/main/java/com/jkgroller/identitymanagement/relationship/repository/AuthenticatedSessionToOnlineAccountRelationshipRepository.java new file mode 100644 index 0000000..5282ba7 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/relationship/repository/AuthenticatedSessionToOnlineAccountRelationshipRepository.java @@ -0,0 +1,50 @@ +package com.jkgroller.identitymanagement.relationship.repository; + +import com.jkgroller.identitymanagement.converter.CycleAvoidingMappingContext; +import com.jkgroller.identitymanagement.converter.IdentityManagementConverter; +import com.jkgroller.identitymanagement.repository.AuthenticatedSessionRepository; +import com.jkgroller.identitymanagement.resource.AuthenticatedSessionResource; +import com.jkgroller.identitymanagement.resource.OnlineAccountResource; +import io.crnk.core.queryspec.QuerySpec; +import io.crnk.core.repository.OneRelationshipRepository; +import io.crnk.core.repository.RelationshipMatcher; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + + +public class AuthenticatedSessionToOnlineAccountRelationshipRepository implements OneRelationshipRepository { + + @Autowired + private AuthenticatedSessionRepository authenticatedSessionRepository; + + @Autowired + private IdentityManagementConverter identityManagementConverter; + + private CycleAvoidingMappingContext cycleAvoidingMappingContext; + + @Override + public RelationshipMatcher getMatcher() { + return new RelationshipMatcher().rule().target(OnlineAccountResource.class).add(); + } + + @Override + public void setRelation(AuthenticatedSessionResource source, String targetId, String fieldName) { + + } + + @Override + public Map findOneRelations(Collection collection, String s, QuerySpec querySpec) { + + Map onlineAccountResources = new HashMap(); + + for (String identifier : collection) { + OnlineAccountResource onlineAccountResource = identityManagementConverter.onlineAccountEntityToOnlineAccountResource(authenticatedSessionRepository.findByIdentifier(identifier).getOnlineAccount(), cycleAvoidingMappingContext); + onlineAccountResources.put(identifier, onlineAccountResource); + } + + return onlineAccountResources; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/relationship/repository/IndividualCustomerToOnlineAccountRelationshipRepository.java b/src/main/java/com/jkgroller/identitymanagement/relationship/repository/IndividualCustomerToOnlineAccountRelationshipRepository.java new file mode 100644 index 0000000..05fa14a --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/relationship/repository/IndividualCustomerToOnlineAccountRelationshipRepository.java @@ -0,0 +1,50 @@ +package com.jkgroller.identitymanagement.relationship.repository; + +import com.jkgroller.identitymanagement.converter.CycleAvoidingMappingContext; +import com.jkgroller.identitymanagement.converter.IdentityManagementConverter; +import com.jkgroller.identitymanagement.repository.IndividualCustomerRepository; +import com.jkgroller.identitymanagement.resource.IndividualCustomerResource; +import com.jkgroller.identitymanagement.resource.OnlineAccountResource; +import io.crnk.core.queryspec.QuerySpec; +import io.crnk.core.repository.OneRelationshipRepository; +import io.crnk.core.repository.RelationshipMatcher; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +// +public class IndividualCustomerToOnlineAccountRelationshipRepository implements OneRelationshipRepository { + + @Autowired + private IndividualCustomerRepository individualCustomerRepository; + + @Autowired + private IdentityManagementConverter identityManagementConverter; + + private CycleAvoidingMappingContext cycleAvoidingMappingContext; + + @Override + public RelationshipMatcher getMatcher() { + return new RelationshipMatcher().rule().target(OnlineAccountResource.class).add(); + } + + @Override + public void setRelation(IndividualCustomerResource individualCustomerResource, String s, String s2) { + // Nothing + } + + @Override + public Map findOneRelations(Collection collection, String s, QuerySpec querySpec) { + + Map onlineAccountResources = new HashMap(); + + for (String identifier : collection) { + OnlineAccountResource onlineAccountResource = identityManagementConverter.onlineAccountEntityToOnlineAccountResource(individualCustomerRepository.findByIdentifier(identifier).getOnlineAccount(), cycleAvoidingMappingContext); + onlineAccountResources.put(identifier, onlineAccountResource); + } + + return onlineAccountResources; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/repository/AuthenticatedSessionRepository.java b/src/main/java/com/jkgroller/identitymanagement/repository/AuthenticatedSessionRepository.java new file mode 100644 index 0000000..5b9177a --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/repository/AuthenticatedSessionRepository.java @@ -0,0 +1,17 @@ +package com.jkgroller.identitymanagement.repository; + +import com.jkgroller.identitymanagement.entity.AuthenticatedSessionEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.List; + +@Transactional +public interface AuthenticatedSessionRepository extends JpaRepository { + + public AuthenticatedSessionEntity findByIdentifier(String identifier); + + public List findByIdentifierIn(Collection identifiers); + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/repository/IndividualCustomerRepository.java b/src/main/java/com/jkgroller/identitymanagement/repository/IndividualCustomerRepository.java new file mode 100644 index 0000000..c529b57 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/repository/IndividualCustomerRepository.java @@ -0,0 +1,17 @@ +package com.jkgroller.identitymanagement.repository; + +import com.jkgroller.identitymanagement.entity.IndividualCustomerEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.List; + +@Transactional +public interface IndividualCustomerRepository extends JpaRepository { + + public IndividualCustomerEntity findByIdentifier(String identifier); + + public List findByIdentifierIn(Collection identifiers); + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/repository/OnlineAccountRepository.java b/src/main/java/com/jkgroller/identitymanagement/repository/OnlineAccountRepository.java new file mode 100644 index 0000000..7d52622 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/repository/OnlineAccountRepository.java @@ -0,0 +1,19 @@ +package com.jkgroller.identitymanagement.repository; + +import com.jkgroller.identitymanagement.entity.OnlineAccountEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.List; + +@Transactional +public interface OnlineAccountRepository extends JpaRepository { + + public OnlineAccountEntity findByIdentifier(String identifier); + + public OnlineAccountEntity findByUserName(String userName); + + public List findByIdentifierIn(Collection identifiers); + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/resource/AuthenticatedSessionResource.java b/src/main/java/com/jkgroller/identitymanagement/resource/AuthenticatedSessionResource.java new file mode 100644 index 0000000..c628af1 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/resource/AuthenticatedSessionResource.java @@ -0,0 +1,111 @@ +package com.jkgroller.identitymanagement.resource; + +import io.crnk.core.resource.annotations.JsonApiField; +import io.crnk.core.resource.annotations.JsonApiId; +import io.crnk.core.resource.annotations.JsonApiRelation; +import io.crnk.core.resource.annotations.JsonApiResource; + +@JsonApiResource(type = "authenticatedSession", resourcePath = "authenticatedSessions") +public class AuthenticatedSessionResource { + + @JsonApiId + private String id; + + @JsonApiField(postable = false, readable = true, patchable = false) + private String tokenType; + + @JsonApiField(postable = false, readable = true, patchable = false) + private String expiresIn; + + @JsonApiField(postable = true, readable = true, patchable = false) + private String userName; + + @JsonApiField(postable = true, readable = false, patchable = false) + private String password; + + @JsonApiField(postable = false, readable = true, patchable = false) + private String token; + + @JsonApiField(postable = false, readable = true, patchable = false) + private String levelOfAssurance; + + @JsonApiField(readable = false, postable = false, patchable = false, deletable = false) + private long dbId; + + @JsonApiRelation + private OnlineAccountResource onlineAccount; + + public long getDbId() { + return dbId; + } + + public void setDbId(long dbId) { + this.dbId = dbId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public void setOnlineAccount(OnlineAccountResource onlineAccount) { + this.onlineAccount = onlineAccount; + } + + public OnlineAccountResource getOnlineAccount() { + return onlineAccount; + } + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + public String getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(String expiresIn) { + this.expiresIn = expiresIn; + } + + public String getLevelOfAssurance() { + return levelOfAssurance; + } + + public void setLevelOfAssurance(String levelOfAssurance) { + this.levelOfAssurance = levelOfAssurance; + } + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/resource/IndividualCustomerResource.java b/src/main/java/com/jkgroller/identitymanagement/resource/IndividualCustomerResource.java new file mode 100644 index 0000000..1632913 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/resource/IndividualCustomerResource.java @@ -0,0 +1,73 @@ +package com.jkgroller.identitymanagement.resource; + +import io.crnk.core.resource.annotations.JsonApiField; +import io.crnk.core.resource.annotations.JsonApiId; +import io.crnk.core.resource.annotations.JsonApiRelation; +import io.crnk.core.resource.annotations.JsonApiResource; + +@JsonApiResource(type = "individualCustomer", resourcePath = "individualCustomers") +public class IndividualCustomerResource { + + @JsonApiId + private String id; + + private String firstName; + + private String lastName; + + private String emailAddress; + + @JsonApiField(readable = false, postable = false, patchable = false, deletable = false) + private long dbId; + + @JsonApiRelation(mappedBy="individualCustomer") + private OnlineAccountResource onlineAccount; + + public OnlineAccountResource getOnlineAccount() { + return onlineAccount; + } + + public void setOnlineAccount(OnlineAccountResource onlineAccount) { + this.onlineAccount = onlineAccount; + } + + 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 getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } + + public long getDbId() { + return dbId; + } + + public void setDbId(long dbId) { + this.dbId = dbId; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/resource/OnlineAccountResource.java b/src/main/java/com/jkgroller/identitymanagement/resource/OnlineAccountResource.java new file mode 100644 index 0000000..3fd7388 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/resource/OnlineAccountResource.java @@ -0,0 +1,118 @@ +package com.jkgroller.identitymanagement.resource; + +import com.jkgroller.identitymanagement.meta.OnlineAccountMeta; +import io.crnk.core.resource.annotations.*; + +@JsonApiResource(type = "onlineAccount", resourcePath = "onlineAccounts") +public class OnlineAccountResource { + + @JsonApiId + private String id; + + @JsonApiField(readable = false, postable = false, patchable = false, deletable = false) + private long dbId; + + @JsonApiField(readable = true, postable = false, patchable = false, deletable = false) + private String creationDate; + + @JsonApiField(readable = true, postable = false, patchable = true, deletable = false) + private String lastLoginDate; + + @JsonApiField(readable = true, postable = true, patchable = true, deletable = false) + private String userName; + + @JsonApiField(readable = false) + private String password; + + @JsonApiField(readable = false, postable = false, patchable = true, deletable = false) + private String oneTimePassword; + + @JsonApiRelation + private IndividualCustomerResource individualCustomer; + + @JsonApiRelation(mappedBy = "onlineAccount") + private AuthenticatedSessionResource authenticatedSession; + + @JsonApiMetaInformation + private OnlineAccountMeta onlineAccountMeta; + + public AuthenticatedSessionResource getAuthenticatedSession() { + return authenticatedSession; + } + + public String getOneTimePassword() { + return oneTimePassword; + } + + public void setOneTimePassword(String oneTimePassword) { + this.oneTimePassword = oneTimePassword; + } + + public void setAuthenticatedSession(AuthenticatedSessionResource authenticatedSession) { + this.authenticatedSession = authenticatedSession; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getCreationDate() { + return creationDate; + } + + public void setCreationDate(String creationDate) { + this.creationDate = creationDate; + } + + public String getLastLoginDate() { + return lastLoginDate; + } + + public void setLastLoginDate(String lastLoginDate) { + this.lastLoginDate = lastLoginDate; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public IndividualCustomerResource getIndividualCustomer() { + return individualCustomer; + } + + public void setIndividualCustomer(IndividualCustomerResource individualCustomer) { + this.individualCustomer = individualCustomer; + } + + public OnlineAccountMeta getOnlineAccountMeta() { + return onlineAccountMeta; + } + + public void setOnlineAccountMeta(OnlineAccountMeta onlineAccountMeta) { + this.onlineAccountMeta = onlineAccountMeta; + } + + public long getDbId() { + return dbId; + } + + public void setDbId(long dbId) { + this.dbId = dbId; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/resource/repository/AuthenticatedSessionResourceRepository.java b/src/main/java/com/jkgroller/identitymanagement/resource/repository/AuthenticatedSessionResourceRepository.java new file mode 100644 index 0000000..d15b0bf --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/resource/repository/AuthenticatedSessionResourceRepository.java @@ -0,0 +1,144 @@ +package com.jkgroller.identitymanagement.resource.repository; + +import com.jkgroller.identitymanagement.converter.CycleAvoidingMappingContext; +import com.jkgroller.identitymanagement.converter.IdentityManagementConverter; +import com.jkgroller.identitymanagement.entity.AuthenticatedSessionEntity; +import com.jkgroller.identitymanagement.entity.OnlineAccountEntity; +import com.jkgroller.identitymanagement.repository.AuthenticatedSessionRepository; +import com.jkgroller.identitymanagement.resource.AuthenticatedSessionResource; +import com.jkgroller.identitymanagement.service.AuthenticationService; +import com.jkgroller.identitymanagement.to.AuthenticationRequestTO; +import io.crnk.core.exception.UnauthorizedException; +import io.crnk.core.queryspec.QuerySpec; +import io.crnk.core.repository.ResourceRepository; +import io.crnk.core.resource.list.ResourceList; +import org.apache.commons.codec.binary.Hex; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Random; + +@Component +public class AuthenticatedSessionResourceRepository + implements ResourceRepository { + + @Autowired + private AuthenticatedSessionRepository authenticatedSessionRepository; + + @Autowired + private IdentityManagementConverter identityManagementConverter; + + @Autowired + private AuthenticationService authenticationService; + + @Autowired + @Qualifier("identifierGenerator") + private BytesKeyGenerator identifierGenerator; + + @Autowired + @Qualifier("tokenGenerator") + private BytesKeyGenerator tokenGenerator; + + @Autowired + private CycleAvoidingMappingContext cycleAvoidingMappingContext; + + @Override + public Class getResourceClass() { + + return AuthenticatedSessionResource.class; + + } + + @Override + public AuthenticatedSessionResource findOne(String s, QuerySpec querySpec) { + + return identityManagementConverter.authenticatedSessionEntityToAuthenticatedSessionResource( + authenticatedSessionRepository.findByIdentifier(s), cycleAvoidingMappingContext); + + } + + @Override + public ResourceList findAll(QuerySpec querySpec) { + + return querySpec.apply(identityManagementConverter + .authenticatedSessionEntitiesToAuthenticatedSessionResources(authenticatedSessionRepository.findAll(), cycleAvoidingMappingContext)); + + } + + @Override + public ResourceList findAll(Collection ids, QuerySpec querySpec) { + + return querySpec.apply(identityManagementConverter + .authenticatedSessionEntitiesToAuthenticatedSessionResources(authenticatedSessionRepository.findByIdentifierIn(ids), cycleAvoidingMappingContext)); + + } + + @Override + public S save(S s) { + + AuthenticatedSessionEntity authenticatedSessionEntity = identityManagementConverter + .authenticatedSessionResourceToAuthenticatedSessionEntity(s, cycleAvoidingMappingContext); + + authenticatedSessionEntity = authenticatedSessionRepository.save(authenticatedSessionEntity); + + return (S) identityManagementConverter + .authenticatedSessionEntityToAuthenticatedSessionResource(authenticatedSessionEntity, cycleAvoidingMappingContext); + } + + @Override + public S create(S s) { + + // Authenticate online account + AuthenticationRequestTO authenticationRequestTO = new AuthenticationRequestTO(); + authenticationRequestTO.setAuthenticatedSessionResource(s); + + OnlineAccountEntity onlineAccountEntity = authenticationService + .authenticateOnlineAccount(authenticationRequestTO); + + if (null == onlineAccountEntity) { + throw new UnauthorizedException("Failed to authenticate."); + } + + AuthenticatedSessionEntity authenticatedSessionEntity = identityManagementConverter + .authenticatedSessionResourceToAuthenticatedSessionEntity(s, cycleAvoidingMappingContext); + + // Generating the identifier, and faking out everything else + authenticatedSessionEntity.setIdentifier(Hex.encodeHexString(identifierGenerator.generateKey())); + authenticatedSessionEntity.setToken(Hex.encodeHexString(tokenGenerator.generateKey())); + authenticatedSessionEntity.setLevelOfAssurance(generateLevelOfAssurance()); + authenticatedSessionEntity.setExpiresIn("15"); + authenticatedSessionEntity.setTokenType("sso"); + + // Save the resource. + authenticatedSessionEntity = authenticatedSessionRepository.save(authenticatedSessionEntity); + + return (S) identityManagementConverter + .authenticatedSessionEntityToAuthenticatedSessionResource(authenticatedSessionEntity, cycleAvoidingMappingContext); + } + + @Override + public void delete(String s) { + AuthenticatedSessionEntity authenticatedSessionEntity = authenticatedSessionRepository.findByIdentifier(s); + authenticatedSessionRepository.delete(authenticatedSessionEntity); + } + + /** + * Generate random level of assurance. + * + * @return + */ + private String generateLevelOfAssurance() { + + Random rand = new Random(); + + int minimum = 1; + int maximum = 4; + + return String.valueOf(rand.nextInt((maximum - minimum) + 1) + minimum); + + } + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/resource/repository/IndividualCustomerResourceRepository.java b/src/main/java/com/jkgroller/identitymanagement/resource/repository/IndividualCustomerResourceRepository.java new file mode 100644 index 0000000..92eea5c --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/resource/repository/IndividualCustomerResourceRepository.java @@ -0,0 +1,101 @@ +package com.jkgroller.identitymanagement.resource.repository; + +import com.jkgroller.identitymanagement.converter.CycleAvoidingMappingContext; +import com.jkgroller.identitymanagement.converter.IdentityManagementConverter; +import com.jkgroller.identitymanagement.entity.IndividualCustomerEntity; +import com.jkgroller.identitymanagement.repository.IndividualCustomerRepository; +import com.jkgroller.identitymanagement.resource.IndividualCustomerResource; +import io.crnk.core.queryspec.QuerySpec; +import io.crnk.core.repository.ResourceRepository; +import io.crnk.core.resource.list.ResourceList; +import org.apache.commons.codec.binary.Hex; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +@Component +public class IndividualCustomerResourceRepository implements ResourceRepository { + + @Autowired + private IndividualCustomerRepository individualCustomerRepository; + + @Autowired + private IdentityManagementConverter identityManagementConverter; + + @Autowired + @Qualifier("identifierGenerator") + private BytesKeyGenerator identifierGenerator; + + @Autowired + private CycleAvoidingMappingContext cycleAvoidingMappingContext; + + @Override + public Class getResourceClass() { + + return IndividualCustomerResource.class; + + } + + @Override + public IndividualCustomerResource findOne(String s, QuerySpec querySpec) { + + IndividualCustomerEntity individualCustomerEntity = individualCustomerRepository.findByIdentifier(s); + + return identityManagementConverter + .individualCustomerEntityToIndividualCustomerResource(individualCustomerEntity, cycleAvoidingMappingContext); + + } + + @Override + public ResourceList findAll(QuerySpec querySpec) { + + return querySpec.apply(identityManagementConverter + .individualCustomerEntitiesToIndividualCustomerResources(individualCustomerRepository.findAll(),cycleAvoidingMappingContext)); + + } + + @Override + public ResourceList findAll(Collection ids, QuerySpec querySpec) { + + return querySpec.apply(identityManagementConverter + .individualCustomerEntitiesToIndividualCustomerResources(individualCustomerRepository.findByIdentifierIn(ids), cycleAvoidingMappingContext)); + + } + + @Override + public S save(S s) { + + IndividualCustomerEntity individualCustomerEntity = identityManagementConverter + .individualCustomerResourceToIndividualCustomerEntity(s, cycleAvoidingMappingContext); + + individualCustomerEntity = individualCustomerRepository.save(individualCustomerEntity); + + return (S) identityManagementConverter + .individualCustomerEntityToIndividualCustomerResource(individualCustomerEntity, cycleAvoidingMappingContext); + } + + @Override + public S create(S s) { + + IndividualCustomerEntity individualCustomerEntity = identityManagementConverter + .individualCustomerResourceToIndividualCustomerEntity(s, cycleAvoidingMappingContext); + + individualCustomerEntity.setIdentifier(Hex.encodeHexString(identifierGenerator.generateKey())); + individualCustomerEntity = individualCustomerRepository.save(individualCustomerEntity); + + return (S) identityManagementConverter + .individualCustomerEntityToIndividualCustomerResource(individualCustomerEntity, cycleAvoidingMappingContext); + } + + @Override + public void delete(String s) { + + IndividualCustomerEntity individualCustomerEntity = individualCustomerRepository.findByIdentifier(s); + individualCustomerRepository.delete(individualCustomerEntity); + + } + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/resource/repository/OnlineAccountResourceRepository.java b/src/main/java/com/jkgroller/identitymanagement/resource/repository/OnlineAccountResourceRepository.java new file mode 100644 index 0000000..9d1d457 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/resource/repository/OnlineAccountResourceRepository.java @@ -0,0 +1,145 @@ +package com.jkgroller.identitymanagement.resource.repository; + +import com.jkgroller.identitymanagement.converter.CycleAvoidingMappingContext; +import com.jkgroller.identitymanagement.converter.IdentityManagementConverter; +import com.jkgroller.identitymanagement.entity.OnlineAccountEntity; +import com.jkgroller.identitymanagement.repository.OnlineAccountRepository; +import com.jkgroller.identitymanagement.resource.OnlineAccountResource; +import com.jkgroller.identitymanagement.service.AuthenticationService; +import com.jkgroller.identitymanagement.service.CreateOnlineAccountService; +import com.jkgroller.identitymanagement.to.CreateOnlineAccountRequestTO; +import com.jkgroller.identitymanagement.to.CreateOnlineAccountResponseTO; +import com.jkgroller.identitymanagement.to.ValidateOneTimePasswordRequestTO; +import com.jkgroller.identitymanagement.to.ValidateOneTimePasswordResponseTO; +import io.crnk.core.exception.ResourceNotFoundException; +import io.crnk.core.queryspec.QuerySpec; +import io.crnk.core.repository.ResourceRepository; +import io.crnk.core.resource.list.ResourceList; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collection; + +@Component +public class OnlineAccountResourceRepository implements ResourceRepository { + + @Autowired + private OnlineAccountRepository onlineAccountRepository; + + @Autowired + private IdentityManagementConverter identityManagementConverter; + + @Autowired + private CreateOnlineAccountService createOnlineAccountService; + + @Autowired + private AuthenticationService authenticationService; + + @Autowired + private CycleAvoidingMappingContext cycleAvoidingMappingContext; + + @Autowired + private ObjectProvider httpServletRequestProvider; + + @Autowired + private ObjectProvider httpServletResponseProvider; + + private final String ACTION_QUERY_PARAM = "action[password]"; + + @Override + public Class getResourceClass() { + + return OnlineAccountResource.class; + + } + + @Override + public OnlineAccountResource findOne(String s, QuerySpec querySpec) { + + return identityManagementConverter + .onlineAccountEntityToOnlineAccountResource(onlineAccountRepository.findByIdentifier(s), cycleAvoidingMappingContext); + + } + + @Override + public ResourceList findAll(QuerySpec querySpec) { + + return querySpec.apply( identityManagementConverter + .onlineAccountEntitiesToOnlineAccountResources(onlineAccountRepository.findAll(),cycleAvoidingMappingContext)); + + } + + @Override + public ResourceList findAll(Collection ids, QuerySpec querySpec) { + + return querySpec.apply(identityManagementConverter + .onlineAccountEntitiesToOnlineAccountResources(onlineAccountRepository.findByIdentifierIn(ids),cycleAvoidingMappingContext)); + + } + + @Override + public S save(S s) { + + HttpServletRequest httpServletRequest = httpServletRequestProvider.getObject(); + + String passwordAction = httpServletRequest.getParameter(ACTION_QUERY_PARAM); + + OnlineAccountEntity onlineAccountEntity = identityManagementConverter + .onlineAccountResourceToOnlineAccountEntity(s, cycleAvoidingMappingContext); + + if(StringUtils.isBlank(passwordAction)) { + onlineAccountEntity = onlineAccountRepository.save(onlineAccountEntity); + }else if("sendOneTimePassword".equals(passwordAction)){ + // Send email with OTP, then set it as the online account's password. Hardcoded here for demonstration purposes. action[password]=sendOneTimePassword + onlineAccountEntity.setPasswordStatus("otp pending"); + onlineAccountEntity.setOneTimePassword("12345"); + onlineAccountEntity = onlineAccountRepository.save(onlineAccountEntity); + }else if("validateOneTimePassword".equals(passwordAction)) { + // Email was received. They go somewhere that allows them to enter the OTP. Patch again to this resource with action[password]=validateOneTimePassword + ValidateOneTimePasswordRequestTO validateOneTimePasswordRequestTO = new ValidateOneTimePasswordRequestTO(); + + validateOneTimePasswordRequestTO.setOnlineAccountResource(s); + ValidateOneTimePasswordResponseTO validateOneTimePasswordResponseTO = authenticationService.validateOnlineAccountWithOneTimePassword(validateOneTimePasswordRequestTO); + + if (null != validateOneTimePasswordResponseTO.getOnlineAccountEntity()) { // One time password validated + onlineAccountEntity.setPasswordStatus("otp validated"); + onlineAccountEntity = onlineAccountRepository.save(onlineAccountEntity); + } + + } + + // After all this, the client should be able to patch the updated password without the parameters. + + return (S) identityManagementConverter.onlineAccountEntityToOnlineAccountResource(onlineAccountEntity,cycleAvoidingMappingContext); + + } + + @Override + public S create(S s) { + + CreateOnlineAccountRequestTO createOnlineAccountRequestTO = new CreateOnlineAccountRequestTO(); + createOnlineAccountRequestTO.setOnlineAccountResource(s); + CreateOnlineAccountResponseTO createOnlineAccountResponseTO = createOnlineAccountService.createOnlineAccount(createOnlineAccountRequestTO); + + return (S) createOnlineAccountResponseTO.getOnlineAccountResource(); + + } + + @Override + public void delete(String s) { + + OnlineAccountEntity onlineAccountEntity = onlineAccountRepository.findByIdentifier(s); + + if(null == onlineAccountEntity){ + throw new ResourceNotFoundException("Online account does not exist."); + } + + onlineAccountRepository.delete(onlineAccountEntity); + + } + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/service/AuthenticationService.java b/src/main/java/com/jkgroller/identitymanagement/service/AuthenticationService.java new file mode 100644 index 0000000..bb7fdeb --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/service/AuthenticationService.java @@ -0,0 +1,14 @@ +package com.jkgroller.identitymanagement.service; + +import com.jkgroller.identitymanagement.entity.OnlineAccountEntity; +import com.jkgroller.identitymanagement.to.AuthenticationRequestTO; +import com.jkgroller.identitymanagement.to.ValidateOneTimePasswordRequestTO; +import com.jkgroller.identitymanagement.to.ValidateOneTimePasswordResponseTO; + +public interface AuthenticationService { + + OnlineAccountEntity authenticateOnlineAccount(AuthenticationRequestTO authenticationRequestTO); + + ValidateOneTimePasswordResponseTO validateOnlineAccountWithOneTimePassword(ValidateOneTimePasswordRequestTO validateOneTimePasswordRequestTO); + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/service/AuthenticationServiceImpl.java b/src/main/java/com/jkgroller/identitymanagement/service/AuthenticationServiceImpl.java new file mode 100644 index 0000000..b6f56da --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/service/AuthenticationServiceImpl.java @@ -0,0 +1,44 @@ +package com.jkgroller.identitymanagement.service; + +import com.jkgroller.identitymanagement.entity.OnlineAccountEntity; +import com.jkgroller.identitymanagement.repository.OnlineAccountRepository; +import com.jkgroller.identitymanagement.to.AuthenticationRequestTO; +import com.jkgroller.identitymanagement.to.ValidateOneTimePasswordRequestTO; +import com.jkgroller.identitymanagement.to.ValidateOneTimePasswordResponseTO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class AuthenticationServiceImpl implements AuthenticationService { + + @Autowired + private OnlineAccountRepository onlineAccountRepository; + + @Override + public OnlineAccountEntity authenticateOnlineAccount(AuthenticationRequestTO authenticationRequestTO){ + + OnlineAccountEntity onlineAccountEntity = onlineAccountRepository.findByUserName(authenticationRequestTO.getAuthenticatedSessionResource().getUserName()); + + if(null != onlineAccountEntity && authenticationRequestTO.getAuthenticatedSessionResource().getPassword().equals(onlineAccountEntity.getPassword())){ + return onlineAccountEntity; + } + + return null; + + } + + @Override + public ValidateOneTimePasswordResponseTO validateOnlineAccountWithOneTimePassword(ValidateOneTimePasswordRequestTO validateOneTimePasswordRequestTO) { + + OnlineAccountEntity onlineAccountEntity = onlineAccountRepository.findByUserName(validateOneTimePasswordRequestTO.getOnlineAccountResource().getUserName()); + ValidateOneTimePasswordResponseTO validateOneTimePasswordResponseTO = new ValidateOneTimePasswordResponseTO(); + + if(null != onlineAccountEntity && validateOneTimePasswordRequestTO.getOnlineAccountResource().getOneTimePassword().equals(onlineAccountEntity.getOneTimePassword())){ + validateOneTimePasswordResponseTO.setOnlineAccountEntity(onlineAccountEntity); + return validateOneTimePasswordResponseTO; + } + + return validateOneTimePasswordResponseTO; + } + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/service/CreateOnlineAccountService.java b/src/main/java/com/jkgroller/identitymanagement/service/CreateOnlineAccountService.java new file mode 100644 index 0000000..1569c69 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/service/CreateOnlineAccountService.java @@ -0,0 +1,10 @@ +package com.jkgroller.identitymanagement.service; + +import com.jkgroller.identitymanagement.to.CreateOnlineAccountRequestTO; +import com.jkgroller.identitymanagement.to.CreateOnlineAccountResponseTO; + +public interface CreateOnlineAccountService { + + CreateOnlineAccountResponseTO createOnlineAccount(CreateOnlineAccountRequestTO createOnlineAccountRequestTO); + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/service/CreateOnlineAccountServiceImpl.java b/src/main/java/com/jkgroller/identitymanagement/service/CreateOnlineAccountServiceImpl.java new file mode 100644 index 0000000..462786f --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/service/CreateOnlineAccountServiceImpl.java @@ -0,0 +1,82 @@ +package com.jkgroller.identitymanagement.service; + +import com.jkgroller.identitymanagement.converter.CycleAvoidingMappingContext; +import com.jkgroller.identitymanagement.converter.IdentityManagementConverter; +import com.jkgroller.identitymanagement.entity.OnlineAccountEntity; +import com.jkgroller.identitymanagement.exception.CreateOnlineAccountConflictException; +import com.jkgroller.identitymanagement.repository.OnlineAccountRepository; +import com.jkgroller.identitymanagement.resource.OnlineAccountResource; +import com.jkgroller.identitymanagement.to.CreateOnlineAccountRequestTO; +import com.jkgroller.identitymanagement.to.CreateOnlineAccountResponseTO; +import org.apache.commons.codec.binary.Hex; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; + +@Service +public class CreateOnlineAccountServiceImpl implements CreateOnlineAccountService { + + @Autowired + private OnlineAccountRepository onlineAccountRepository; + + @Autowired + private IdentityManagementConverter identityManagementConverter; + + @Autowired + private CycleAvoidingMappingContext cycleAvoidingMappingContext; + + @Autowired + @Qualifier("identifierGenerator") + private BytesKeyGenerator identifierGenerator; + + public CreateOnlineAccountResponseTO createOnlineAccount(CreateOnlineAccountRequestTO createOnlineAccountRequestTO){ + + // Check if username is taken. + verifyUserNameAvailable(createOnlineAccountRequestTO.getOnlineAccountResource().getUserName()); + + CreateOnlineAccountResponseTO createOnlineAccountResponseTO = new CreateOnlineAccountResponseTO(); + + // Convert resource to entity. + OnlineAccountEntity onlineAccountEntity = identityManagementConverter + .onlineAccountResourceToOnlineAccountEntity(createOnlineAccountRequestTO.getOnlineAccountResource(),cycleAvoidingMappingContext); + + // Set some values related to account creation + onlineAccountEntity.setAccountStatus("active"); + onlineAccountEntity.setPasswordStatus("active"); + onlineAccountEntity.setIdentifier(Hex.encodeHexString(identifierGenerator.generateKey())); + onlineAccountEntity.setCreationDate(LocalDateTime.now().toString()); + onlineAccountEntity.setLastLoginDate(LocalDateTime.now().toString()); + + // Save new account + onlineAccountEntity = onlineAccountRepository.save(onlineAccountEntity); + + // Convert it back to the resource + OnlineAccountResource onlineAccountResource = identityManagementConverter.onlineAccountEntityToOnlineAccountResource(onlineAccountEntity, cycleAvoidingMappingContext); + + // Set response + createOnlineAccountResponseTO.setOnlineAccountResource(onlineAccountResource); + + // Return it + return createOnlineAccountResponseTO; + + } + /** + * Check if username is taken. If so, throw exception. + * @param userName + */ + private void verifyUserNameAvailable(String userName){ + + OnlineAccountEntity onlineAccountEntity = onlineAccountRepository.findByUserName(userName); + + if(null != onlineAccountEntity){ + throw new CreateOnlineAccountConflictException(); + } + + } + + + +} diff --git a/src/main/java/com/jkgroller/identitymanagement/to/AuthenticationRequestTO.java b/src/main/java/com/jkgroller/identitymanagement/to/AuthenticationRequestTO.java new file mode 100644 index 0000000..64468d1 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/to/AuthenticationRequestTO.java @@ -0,0 +1,16 @@ +package com.jkgroller.identitymanagement.to; + +import com.jkgroller.identitymanagement.resource.AuthenticatedSessionResource; + +public class AuthenticationRequestTO { + + private AuthenticatedSessionResource authenticatedSessionResource; + + public AuthenticatedSessionResource getAuthenticatedSessionResource() { + return authenticatedSessionResource; + } + + public void setAuthenticatedSessionResource(AuthenticatedSessionResource authenticatedSessionResource) { + this.authenticatedSessionResource = authenticatedSessionResource; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/to/CreateOnlineAccountRequestTO.java b/src/main/java/com/jkgroller/identitymanagement/to/CreateOnlineAccountRequestTO.java new file mode 100644 index 0000000..d678048 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/to/CreateOnlineAccountRequestTO.java @@ -0,0 +1,16 @@ +package com.jkgroller.identitymanagement.to; + +import com.jkgroller.identitymanagement.resource.OnlineAccountResource; + +public class CreateOnlineAccountRequestTO { + + private OnlineAccountResource onlineAccountResource; + + public OnlineAccountResource getOnlineAccountResource() { + return onlineAccountResource; + } + + public void setOnlineAccountResource(OnlineAccountResource onlineAccountResource) { + this.onlineAccountResource = onlineAccountResource; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/to/CreateOnlineAccountResponseTO.java b/src/main/java/com/jkgroller/identitymanagement/to/CreateOnlineAccountResponseTO.java new file mode 100644 index 0000000..ef010f0 --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/to/CreateOnlineAccountResponseTO.java @@ -0,0 +1,16 @@ +package com.jkgroller.identitymanagement.to; + +import com.jkgroller.identitymanagement.resource.OnlineAccountResource; + +public class CreateOnlineAccountResponseTO { + + private OnlineAccountResource onlineAccountResource; + + public OnlineAccountResource getOnlineAccountResource() { + return onlineAccountResource; + } + + public void setOnlineAccountResource(OnlineAccountResource onlineAccountResource) { + this.onlineAccountResource = onlineAccountResource; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/to/ValidateOneTimePasswordRequestTO.java b/src/main/java/com/jkgroller/identitymanagement/to/ValidateOneTimePasswordRequestTO.java new file mode 100644 index 0000000..5ae8dbe --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/to/ValidateOneTimePasswordRequestTO.java @@ -0,0 +1,16 @@ +package com.jkgroller.identitymanagement.to; + +import com.jkgroller.identitymanagement.resource.OnlineAccountResource; + +public class ValidateOneTimePasswordRequestTO { + + private OnlineAccountResource onlineAccountResource; + + public OnlineAccountResource getOnlineAccountResource() { + return onlineAccountResource; + } + + public void setOnlineAccountResource(OnlineAccountResource onlineAccountResource) { + this.onlineAccountResource = onlineAccountResource; + } +} diff --git a/src/main/java/com/jkgroller/identitymanagement/to/ValidateOneTimePasswordResponseTO.java b/src/main/java/com/jkgroller/identitymanagement/to/ValidateOneTimePasswordResponseTO.java new file mode 100644 index 0000000..a59969b --- /dev/null +++ b/src/main/java/com/jkgroller/identitymanagement/to/ValidateOneTimePasswordResponseTO.java @@ -0,0 +1,16 @@ +package com.jkgroller.identitymanagement.to; + +import com.jkgroller.identitymanagement.entity.OnlineAccountEntity; + +public class ValidateOneTimePasswordResponseTO { + + private OnlineAccountEntity onlineAccountEntity; + + public OnlineAccountEntity getOnlineAccountEntity() { + return onlineAccountEntity; + } + + public void setOnlineAccountEntity(OnlineAccountEntity onlineAccountEntity) { + this.onlineAccountEntity = onlineAccountEntity; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..c719e72 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,31 @@ +crnk: + path-prefix: /v1 + return404-on-null: true + allow-unknown-parameters: true + +identifier: + length: 8 + +token: + length: 64 + +spring: + datasource: + url: jdbc:h2:mem:IdentityManagement;DB_CLOSE_DELAY=-1 + platform: h2 + username: sa + password: + initialization-mode: ALWAYS + jpa: + show-sql: false + hibernate: + ddl-auto: create-drop + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + h2: + console: + enabled: true + path: /h2 + settings: + web-allow-others: true \ No newline at end of file diff --git a/src/test/resources/identity-management.postman_collection.json b/src/test/resources/identity-management.postman_collection.json new file mode 100644 index 0000000..b26b3f9 --- /dev/null +++ b/src/test/resources/identity-management.postman_collection.json @@ -0,0 +1,449 @@ +{ + "info": { + "_postman_id": "e368ccd5-d7e6-4f94-8c94-21e36005ee44", + "name": "identity-management", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "/individualCustomers", + "item": [ + { + "name": "Create Individual Customer", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {\r\n \"type\": \"individualCustomer\",\r\n \"attributes\": {\r\n \t\"firstName\":\"John\",\r\n \t\"lastName\":\"Groller\",\r\n \t\"emailAddress\":\"john@jkgroller.com\"\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/individualCustomers", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "individualCustomers" + ] + } + }, + "response": [] + }, + { + "name": "Update Individual Customer", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {\r\n \"type\": \"individualCustomer\",\r\n \"attributes\": {\r\n \t\"firstName\":\"jacob\",\r\n \t\"lastName\":\"Jingleheimer\",\r\n \t\"emailAddress\":\"john@jkgroller.com\"\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/individualCustomers/836fc5a53d82554b", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "individualCustomers", + "836fc5a53d82554b" + ] + } + }, + "response": [] + }, + { + "name": "Retrieve One Individual Customer", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/v1/individualCustomers/bd862d9aa270a06a", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "individualCustomers", + "bd862d9aa270a06a" + ] + } + }, + "response": [] + }, + { + "name": "Retrieve All Individual Customers", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8080/v1/individualCustomers", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "individualCustomers" + ] + } + }, + "response": [] + }, + { + "name": "Delete One Individual Customer", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8080/v1/individualCustomers/bd862d9aa270a06a", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "individualCustomers", + "bd862d9aa270a06a" + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} + }, + { + "name": "/onlineAccounts", + "item": [ + { + "name": "Retrieve All Online Accounts", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {\r\n \"type\": \"authenticatedSession\",\r\n \"attributes\": {\r\n \t\"userName\":\"mreid\",\r\n \t\"password\":\"password\"\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/onlineAccounts", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "onlineAccounts" + ] + } + }, + "response": [] + }, + { + "name": "Retrieve One Online Account", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {\r\n \"type\": \"authenticatedSession\",\r\n \"attributes\": {\r\n \t\"userName\":\"mreid\",\r\n \t\"password\":\"password\"\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/onlineAccounts/d9f6d50575868b4a", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "onlineAccounts", + "d9f6d50575868b4a" + ] + } + }, + "response": [] + }, + { + "name": "Delete One Online Account", + "request": { + "method": "DELETE", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {\r\n \"type\": \"authenticatedSession\",\r\n \"attributes\": {\r\n \t\"userName\":\"mreid\",\r\n \t\"password\":\"password\"\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/onlineAccounts/4eb9702aeff4eb7f", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "onlineAccounts", + "4eb9702aeff4eb7f" + ] + } + }, + "response": [] + }, + { + "name": "Create Online Account Related To Individual Customer", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {\r\n \"type\": \"onlineAccount\",\r\n \"attributes\": {\r\n \t\"userName\":\"john\",\r\n \t\"password\":\"password\"\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/individualCustomers/b3ba59ba3fabf6d6/onlineAccount", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "individualCustomers", + "b3ba59ba3fabf6d6", + "onlineAccount" + ] + } + }, + "response": [] + }, + { + "name": "Update Online Account", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {\r\n \"type\": \"onlineAccount\",\r\n \"attributes\": {\r\n \t\"userName\":\"mreid2\",\r\n \t\"password\":\"password\"\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/onlineAccounts/7878356cbaa31769", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "onlineAccounts", + "7878356cbaa31769" + ] + } + }, + "response": [] + }, + { + "name": "Update Online Account - Begin OTP", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {}\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/onlineAccounts/3ff5750dbbb18935?action[password]=sendOneTimePassword", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "onlineAccounts", + "3ff5750dbbb18935" + ], + "query": [ + { + "key": "action[password]", + "value": "sendOneTimePassword" + } + ] + } + }, + "response": [] + }, + { + "name": "Update Online Account - Validate OTP", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {\r\n \"type\": \"onlineAccount\",\r\n \"attributes\": {\r\n \t\"userName\":\"john\",\r\n \t\"oneTimePassword\":\"12345\"\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/onlineAccounts/3ff5750dbbb18935?action[password]=validateOneTimePassword", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "onlineAccounts", + "3ff5750dbbb18935" + ], + "query": [ + { + "key": "action[password]", + "value": "validateOneTimePassword" + } + ] + } + }, + "response": [] + }, + { + "name": "Update Online Account - Password", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {\r\n \"type\": \"onlineAccount\",\r\n \"attributes\": {\r\n \t\"userName\":\"john\",\r\n \t\"password\":\"password\"\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/onlineAccounts/3ff5750dbbb18935", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "onlineAccounts", + "3ff5750dbbb18935" + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} + }, + { + "name": "/authenticatedSessions", + "item": [ + { + "name": "Create Authenticated Session Related To Online Account", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "type": "text", + "value": "application/vnd.api+json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"data\": {\r\n \"type\": \"authenticatedSession\",\r\n \"attributes\": {\r\n \t\"userName\":\"john\",\r\n \t\"password\":\"password\"\r\n }\r\n }\r\n}" + }, + "url": { + "raw": "http://localhost:8080/v1/onlineAccounts/325682df5689397a/authenticatedSession", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "v1", + "onlineAccounts", + "325682df5689397a", + "authenticatedSession" + ] + } + }, + "response": [] + } + ], + "protocolProfileBehavior": {} + } + ], + "protocolProfileBehavior": {} +} \ No newline at end of file