Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.rules.SecurityRule;
import io.micronaut.serde.annotation.Serdeable;
import io.unityfoundation.auth.entities.*;
import io.unityfoundation.auth.entities.Service.ServiceStatus;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;

@Secured(SecurityRule.IS_AUTHENTICATED)
@Secured("USER")
@Controller("/api")
public class AuthController {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.unityfoundation.auth;

import io.micronaut.security.token.reader.HttpHeaderTokenReader;
import jakarta.inject.Singleton;

@Singleton
public class InternalAuthTokenReader extends HttpHeaderTokenReader {

@Override
protected String getPrefix() {
return null;
}

@Override
protected String getHeaderName() {
return "X-Unity-Auth-Internal";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.unityfoundation.auth;

import io.micronaut.context.annotation.Value;
import io.micronaut.http.HttpRequest;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.token.validator.TokenValidator;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;

@Singleton
public class InternalAuthTokenValidator implements TokenValidator<HttpRequest<?>> {

@Value("${unity.auth.internal-token}")
private String internalToken;

@Override
public Publisher<Authentication> validateToken(String token, HttpRequest<?> request) {
if (internalToken != null && MessageDigest.isEqual(
internalToken.getBytes(StandardCharsets.UTF_8),
token.getBytes(StandardCharsets.UTF_8))) {
return Mono.just(Authentication.build("internal-service", List.of("INTERNAL_SERVICE")));
}
return Mono.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package io.unityfoundation.auth;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.annotation.*;
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.security.annotation.Secured;
import io.micronaut.serde.annotation.Serdeable;
import io.unityfoundation.auth.entities.PasswordResetToken;
import io.unityfoundation.auth.entities.PasswordResetTokenRepo;
import io.unityfoundation.auth.entities.User;
import io.unityfoundation.auth.entities.UserRepo;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.HexFormat;
import java.util.Optional;
import java.util.UUID;

@Secured("INTERNAL_SERVICE")
@Controller("/api/password-reset")
public class PasswordResetController {

private final UserRepo userRepo;
private final PasswordResetTokenRepo tokenRepo;
private final PasswordEncoder passwordEncoder;

public PasswordResetController(UserRepo userRepo, PasswordResetTokenRepo tokenRepo, PasswordEncoder passwordEncoder) {
this.userRepo = userRepo;
this.tokenRepo = tokenRepo;
this.passwordEncoder = passwordEncoder;
}

@Post("/generate")
public HttpResponse<GenerateTokenResponse> generateToken(@Body @Valid GenerateTokenRequest request) {
Optional<User> userOptional = userRepo.findByEmail(request.email());
if (userOptional.isEmpty()) {
throw new HttpStatusException(HttpStatus.NOT_FOUND, "User not found");
}

User user = userOptional.get();
tokenRepo.deleteByUserId(user.getId());

String rawToken = UUID.randomUUID().toString();
PasswordResetToken token = new PasswordResetToken();
token.setToken(sha256(rawToken));
token.setUserId(user.getId());
token.setExpiry(Instant.now().plus(1, ChronoUnit.HOURS));
tokenRepo.save(token);

return HttpResponse.ok(new GenerateTokenResponse(rawToken));
}

@Post("/reset")
@Transactional
public HttpResponse<?> resetPassword(@Body @Valid ResetPasswordRequest request) {
Optional<PasswordResetToken> tokenOptional = tokenRepo.findByToken(sha256(request.token()));

if (tokenOptional.isEmpty()) {
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Invalid token");
}

PasswordResetToken token = tokenOptional.get();
if (token.getExpiry().isBefore(Instant.now())) {
tokenRepo.delete(token);
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Token expired");
}

Optional<User> userOptional = userRepo.findById(token.getUserId());
if (userOptional.isEmpty()) {
throw new HttpStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "User not found");
}

User user = userOptional.get();
user.setPassword(passwordEncoder.encode(request.newPassword()));
userRepo.update(user);

tokenRepo.delete(token);

return HttpResponse.ok();
}

private static String sha256(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return HexFormat.of().formatHex(digest.digest(input.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}

@Serdeable
public record GenerateTokenRequest(@NotBlank String email) {}

@Serdeable
public record GenerateTokenResponse(@NotBlank String token) {}

@Serdeable
public record ResetPasswordRequest(@NotBlank String token, @NotBlank String newPassword) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.util.List;
import java.util.Map;
import java.util.Objects;

Expand Down Expand Up @@ -68,6 +69,7 @@ private User findUser(AuthenticationRequest<?, ?> authRequest) {
} else {
return Mono.just(AuthenticationResponse.success(
(String) authenticationRequest.getIdentity(),
List.of("USER"),
Map.of(
"first_name", Objects.toString(user.getFirstName(), ""),
"last_name", Objects.toString(user.getLastName(), "")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import io.micronaut.http.exceptions.HttpStatusException;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.rules.SecurityRule;
import io.micronaut.serde.annotation.Serdeable;
import io.unityfoundation.auth.entities.*;
import jakarta.transaction.Transactional;
Expand All @@ -18,7 +17,7 @@
import java.util.Objects;
import java.util.Optional;

@Secured(SecurityRule.IS_AUTHENTICATED)
@Secured("USER")
@Controller("/api/users")
public class UserController {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.unityfoundation.auth.entities;

import io.micronaut.core.annotation.Introspected;
import io.micronaut.data.annotation.GeneratedValue;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.NotNull;

import java.time.Instant;

@Serdeable
@MappedEntity
public class PasswordResetToken {
@Id
@GeneratedValue
private Long id;

@NotNull
private String token;

@NotNull
private Long userId;

@NotNull
private Instant expiry;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}

public Long getUserId() {
return userId;
}

public void setUserId(Long userId) {
this.userId = userId;
}

public Instant getExpiry() {
return expiry;
}

public void setExpiry(Instant expiry) {
this.expiry = expiry;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.unityfoundation.auth.entities;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

import java.util.Optional;

@JdbcRepository(dialect = Dialect.MYSQL)
public interface PasswordResetTokenRepo extends CrudRepository<PasswordResetToken, Long> {
Optional<PasswordResetToken> findByToken(String token);
void deleteByUserId(Long userId);
}
4 changes: 4 additions & 0 deletions UnityAuth/src/main/resources/application-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ flyway:
- classpath:db/migration
- classpath:local

unity:
auth:
internal-token: unity-internal-secret

jwk:
primary: '{"p":"_OZyH1Mk3wR0oXw1C31t4kWOcaHFB6Njro1cYx52REnPiznn_JTtwvlAMpvV6LVCIZPgKMzdIEMY1gYs1LsO-5IFqWwegXmYJ0iKXbRrZshfWBCzRLK3QK5fER1le1XUBDhtDk7KIW_Xg-SZF4pf_LUEVKMnyUpspGI5F77jlJ8","kty":"RSA","q":"s9wvl7z8vkHQvo9xOUp-z0a2Z7LFBDil2uIjPh1FQzs34gFXH8dQPRox83TuN5d4KzdLPqQNQAfMXU9_KmxihNb_qDQahYugeELmcem04munxXqBdyZqWhWCy5YmujYqn44irwvoTbw6_RkMqjCmINPTPadptlPivsZ6RhKn8zk","d":"ok3wmhOy8NZEHAotnFiH6ecFD6xf_9x33_fMRkqa3_KE8NZM7vmvNgElox2UvcP_2K5E7jOdL2XQdJCTIW3Qlj66yE2a84SYlbvxIc4hDrIog0XNt4FhavvshxxUIfDQo6Q8qXDR5v7nwt6SCopYC3t3KVRdJh08GzKoVxysd7afJjxXxx178gY29uMRqnwxFN1OGnWaiBr-xGKb1frJ6jOI1zvuuCaljZ4aZjc9vOR4y9ZmobgrzkMFnpDAmQZ7MWcVMyodRMOA2dEOckywPhg-dIVNiVIqzJqe5Yg1ilNookjwtqj2TpNU7Z9gPqzYB73PmQ2p5LMDheAPxcOmEQ","e":"AQAB","use":"sig","kid":"e3be37177a7c42bcbadd7cc63715f216","qi":"r--nAtaYPAgJq_8R1-kynpd53E17n-loDUgtVWBCx_RmdORX4Auilv1S83dD1mbcnYCbV_LmxiEjOiz-4gS_E0qVGqakAqQrO1hVUvJa_Y2uftDgwFmuJNGbpRU-K4Td_uUzdm48za8yJCgOdYsWp6PNMCcmQgiInzkR3XYV83I","dp":"oQUcvmMSw8gzdin-IB2xW_MLecAVEgLu0dGBdD6N8HbKZQvub_xm0dAfFtnvvWXDAFwFyhR96i-uXX67Bos_Q9-6KSAE4E0KGmDucDESfPOw-QJREbl0QgOD1gLQfVGtVy6SCR0TR2zNXFWtP7bD3MNoSXdEOr5fI97CGSNaBWM","alg":"RS256","dq":"DM-WJDy10-dkMu6MpgQEXEcxHtnA5rgSODD7SaVUFaHWLSbjScQslu2SuUCO5y7GxG0_0spklzb2-356FE98BPI7a4Oqj_COEYLSXzLCS45XeN1s80utL5Vwp4eeYo0RJCQ_nDBA76iEmxp5qHWmn5f25-FQykfXUrdYZj1V8SE","n":"sa6m2i-iNvj6ZSTdSHZaBrnv6DId4AqAXhOyl0yA5fNWYe6r51h24SXqk7DsGYHHh74ii74tP1lTpmy6RD67tCK-tbN-d6yc4Z6FfM8R83v2QZUfaAixgHGtw0n2toqsiHf6EloDV-B8q4GYyKDD6cLecoaIuTmMBTY3kts59U2t9W10YoLGsmFqLSz8qNF5HkahzB6_--2DiBfVGUKAXHC-SICGZCi-8efOetv6pt9vFiWEgwU_DgjRNYzLFt1SEmbGFUU4kbjQ7tNTMkHfzfwcT6qLt4kVKy2FNYsEMk24keWtCvW_RyO_fisZc0W9smX7WtYjEXhcAjDeqHgEZw"}'
secondary: '{"p":"4qJ9RNlu6SuDT_MLArfzimvKEwmet_j12Z9EQeb5nMjZIOHTcWw__duebUytfWwxsRHhtSVXeMt-EryQAOulm2p1bfiVuparq93z9P5cPnb0oArFaw3eFNFEmX5U-lY8PzUTTsFxO4aVQYAKXD6DP7p5uPzuwpHFuNc71nNIXZE","kty":"RSA","q":"v4OhkWMbS_nq77HFanwZAT_obfJuQfOFOQBORL4ATAHGUXm2y4YqLNExZs7Wj1MA_6ya6Y00s2JBM7fWq_fPe4d9xo5aGrPdcp0G8W21kkfh9vuVPlHVQTgSP7FQ9qahvXxNwK_11yNr3p1HBmScJ5mHlMBpIJsFcvHA-uXe0Ps","d":"EunrjnQ1-jJPSCrt2L94PUpDrakup8a4pXys52YSkJY-W6XidM0roOS6kr06P3G6VQgc6AL_BkvTQ_XS0oXHbXVprDQ5Syam5p9oxHBhhW_vSqIMgUOfm28uyB3Mtw9rBxdUxW3yElHioaR8a-exYhhyVXb1QEhxL_rcnthmhAkM2NcHi2UnxGKFTsC0abQ2MuQc1OAuW5veDiIF2hfdC41qE0_d8vB6FDWbblgUpbwB6uSZaViPs15Buq2oX9dCCw54-PgzkfehDt7lyqgupktbV1psnVVhL86shzt4QFnhd3k7VpFbjCNFtiJTrufV-XBWT0pl2w3VR9wrHJ1bYQ","e":"AQAB","use":"sig","kid":"0794e938379540dc8eaa559508524a79","qi":"jy-TNyXVy_44_n4KGAwIbZO2C4r6uNWuEdehBfQKkPhiP90myG1KZVfOoKNOK9bCv2mvZJcBz4c1ArElgpuSCV4-KFac1ZzQo_ic5aoIej8Qa80y2ogc-_Yv6_ZLHC1S76M-lm4jayk2-rvuBpy2pUvHbW6Srhs_szwz7ZfSkLg","dp":"ApqdV9ortRAj7Ro8ySY17SQ56SgWI8T_hiWXUi6GNa_1FrShik8VGSSZ2GWmJKfGlmM_NaadL60e4LY77VbHy1ZYzQ-rIL60cEAXmnwFsU4Kl4AoLoe1QoX5BM53yXyOKqfAdgow898i_eKru82YEnZhCagWUjP8kpgefuNKNJE","alg":"RS256","dq":"bFF78WoXh0pMCdQHL2oPDnjh8kWa_OxKHmpA2nqIWnTqgSyRKd2xPvX2tgooqpmsx-8NEymNdCQPcrv4y_z2OgzxI3tiFRZEGs4bnjOJ7bmAYZv71mqcbi3TjHiyrT6j3jNPGrurFUpweVGFWWVQOMmKOKT3ELz9QPzhREb9Vj8","n":"qYvDpV8DRU5hx9eXpE4Ms8nUXicEwrxUUz5gb5gkXpIeY82mqfQKKCP6PSFnkKYtRFTOUSm9cgGGfOd7O4NFsIsxLwXCj34X7ORr19eXKBLvG3bZJLxqRlbYuQshDMkQOui1sDBxvYnj5p4iHne6l2btH5grHOCShUWG-bKps5Y8bKNHod1pIOOBabVCmn3sUVUkZw8nyXkQqZbv-c8x6z0TEfhNOPOIt2AmmlNgrE_8g7-dnCvqfJnhv0c7qkOJzsb7OMmvVwsQNiM59D6uaWZr-vdANo6NggiZmCKUS3tpUvdXW7ec9WMPJWhrVEkRcbWXQnZ_C7pXFrz7rLeNKw"}'
3 changes: 3 additions & 0 deletions UnityAuth/src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ micronaut:
port: 9090
security:
authentication: bearer
unity:
auth:
internal-token: unity-internal-secret
datasources:
default:
url: jdbc:mysql://localhost:13306/test?allowPublicKeyRetrieval=true&useSSL=false
Expand Down
3 changes: 3 additions & 0 deletions UnityAuth/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ micronaut:
access:
- isAnonymous()
authentication: bearer
unity:
auth:
internal-token: ${UNITY_AUTH_INTERNAL_TOKEN}
server:
cors:
enabled: true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE password_reset_token
(
id bigint AUTO_INCREMENT PRIMARY KEY,
token varchar(255) NOT NULL,
user_id bigint NOT NULL,
expiry timestamp NOT NULL,
UNIQUE (token),
CONSTRAINT password_reset_token_user_FK FOREIGN KEY (user_id) REFERENCES user (id)
);
Loading
Loading