Skip to content

Commit

Permalink
Support LDAP-based user authentication #1371
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesChenX committed Jan 14, 2024
1 parent a5a64ee commit 6e26490
Show file tree
Hide file tree
Showing 22 changed files with 990 additions and 287 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
import java.util.Collections;
import java.util.Set;

import reactor.core.publisher.Mono;

import im.turms.server.common.access.client.dto.request.TurmsRequest;
import im.turms.server.common.access.client.dto.request.TurmsRequestTypePool;
import im.turms.server.common.access.common.ResponseStatusCode;

/**
Expand All @@ -30,7 +33,19 @@ public record UserPermissionInfo(
ResponseStatusCode authenticationCode,
Set<TurmsRequest.KindCase> permissions
) {

public static final UserPermissionInfo GRANTED_WITH_ALL_PERMISSIONS =
new UserPermissionInfo(ResponseStatusCode.OK, TurmsRequestTypePool.ALL);
public static final Mono<UserPermissionInfo> GRANTED_WITH_ALL_PERMISSIONS_MONO =
Mono.just(GRANTED_WITH_ALL_PERMISSIONS);
public static final UserPermissionInfo LOGIN_AUTHENTICATION_FAILED =
new UserPermissionInfo(ResponseStatusCode.LOGIN_AUTHENTICATION_FAILED);
public static final Mono<UserPermissionInfo> LOGIN_AUTHENTICATION_FAILED_MONO =
Mono.just(LOGIN_AUTHENTICATION_FAILED);
public static final Mono<UserPermissionInfo> LOGGING_IN_USER_NOT_ACTIVE_MONO =
Mono.just(new UserPermissionInfo(ResponseStatusCode.LOGGING_IN_USER_NOT_ACTIVE));

public UserPermissionInfo(ResponseStatusCode authenticationCode) {
this(authenticationCode, Collections.emptySet());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package im.turms.gateway.domain.session.repository;

import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Repository;
Expand All @@ -35,10 +36,14 @@
@Repository
public class UserRepository extends BaseRepository<User, Long> {

@Getter
private final boolean enabled;

public UserRepository(
@Autowired(
required = false) @Qualifier("userMongoClient") TurmsMongoClient mongoClient) {
super(mongoClient, User.class);
enabled = mongoClient != null;
}

public Mono<User> findPassword(Long userId) {
Expand All @@ -57,4 +62,4 @@ public Mono<Boolean> isActiveAndNotDeleted(Long userId) {
return mongoClient.exists(User.class, filter);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright (C) 2019 The Turms Project
* https://github.com/turms-im/turms
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.turms.gateway.domain.session.service;

import java.time.Duration;
import java.util.Map;
import java.util.Set;

import io.netty.handler.codec.http.HttpMethod;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;

import im.turms.gateway.access.client.common.authorization.policy.Policy;
import im.turms.gateway.access.client.common.authorization.policy.PolicyDeserializer;
import im.turms.gateway.access.client.common.authorization.policy.PolicyManager;
import im.turms.gateway.domain.session.bo.UserLoginInfo;
import im.turms.gateway.domain.session.bo.UserPermissionInfo;
import im.turms.server.common.access.common.ResponseStatusCode;
import im.turms.server.common.infra.collection.CollectionUtil;
import im.turms.server.common.infra.json.JsonUtil;
import im.turms.server.common.infra.lang.StringUtil;
import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.http.HttpAuthenticationResponseExpectationProperties;
import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.http.HttpIdentityAccessManagementProperties;
import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.http.HttpIdentityAccessManagementRequestProperties;
import im.turms.server.common.infra.validation.Validator;

/**
* @author James Chen
*/
public class HttpSessionIdentityAccessManager implements SessionIdentityAccessManagementSupport {

private final HttpClient httpIdentityAccessManagementClient;
private final HttpMethod httpIdentityAccessManagementHttpMethod;
private final Set<String> httpAuthenticationExpectedStatusCodes;
private final Map<String, String> httpAuthenticationExpectedHeaders;
private final Map<String, Object> httpAuthenticationExpectedBodyFields;

private final PolicyManager policyManager;

public HttpSessionIdentityAccessManager(
HttpIdentityAccessManagementProperties httpProperties,
PolicyManager policyManager) {
HttpIdentityAccessManagementRequestProperties requestProperties =
httpProperties.getRequest();
HttpAuthenticationResponseExpectationProperties responseExpectationProperties =
httpProperties.getAuthentication()
.getResponseExpectation();
String url = requestProperties.getUrl();
Exception exception = Validator.url(url);
if (exception != null) {
throw new IllegalArgumentException(
"Illegal HTTP URL: "
+ url,
exception);
}
this.httpIdentityAccessManagementClient = HttpClient.create()
.baseUrl(url)
.headers(entries -> {
for (Map.Entry<String, String> entry : requestProperties.getHeaders()
.entrySet()) {
entries.add(entry.getKey(), entry.getValue());
}
})
.responseTimeout(Duration.ofMillis(requestProperties.getTimeoutMillis()));
this.httpIdentityAccessManagementHttpMethod =
HttpMethod.valueOf(requestProperties.getHttpMethod()
.name());
this.httpAuthenticationExpectedStatusCodes = responseExpectationProperties.getStatusCodes();
this.httpAuthenticationExpectedHeaders = responseExpectationProperties.getHeaders();
this.httpAuthenticationExpectedBodyFields = responseExpectationProperties.getBodyFields();

this.policyManager = policyManager;
}

@Override
public Mono<UserPermissionInfo> verifyAndGrant(UserLoginInfo userLoginInfo) {
return httpIdentityAccessManagementClient.request(httpIdentityAccessManagementHttpMethod)
.send(Mono.fromCallable(() -> JsonUtil.write(userLoginInfo)))
.responseSingle((response, bodyBufferMono) -> {
if (!StringUtil.matchLatin1(response.status()
.toString(), httpAuthenticationExpectedStatusCodes)) {
return UserPermissionInfo.LOGIN_AUTHENTICATION_FAILED_MONO;
}
for (Map.Entry<String, String> entry : httpAuthenticationExpectedHeaders
.entrySet()) {
if (!entry.getValue()
.equals(response.responseHeaders()
.get(entry.getKey()))) {
return UserPermissionInfo.LOGIN_AUTHENTICATION_FAILED_MONO;
}
}
return bodyBufferMono.asInputStream()
.map(inputStream -> {
Map<String, Object> map;
Policy policy;
try {
map = JsonUtil.readStringObjectMapValue(inputStream);
policy = PolicyDeserializer.parse(map);
} catch (Exception e) {
throw new IllegalArgumentException("Illegal request body", e);
}
if (!CollectionUtil.containsAllLooseComparison(map,
httpAuthenticationExpectedBodyFields)) {
return UserPermissionInfo.LOGIN_AUTHENTICATION_FAILED;
}
return new UserPermissionInfo(
ResponseStatusCode.OK,
policyManager.findAllowedRequestTypes(policy));
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright (C) 2019 The Turms Project
* https://github.com/turms-im/turms
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.turms.gateway.domain.session.service;

import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Map;

import reactor.core.publisher.Mono;

import im.turms.gateway.access.client.common.authorization.policy.IllegalPolicyException;
import im.turms.gateway.access.client.common.authorization.policy.Policy;
import im.turms.gateway.access.client.common.authorization.policy.PolicyDeserializer;
import im.turms.gateway.access.client.common.authorization.policy.PolicyManager;
import im.turms.gateway.domain.session.bo.UserLoginInfo;
import im.turms.gateway.domain.session.bo.UserPermissionInfo;
import im.turms.server.common.access.common.ResponseStatusCode;
import im.turms.server.common.infra.collection.CollectionUtil;
import im.turms.server.common.infra.lang.StringUtil;
import im.turms.server.common.infra.logging.core.logger.Logger;
import im.turms.server.common.infra.logging.core.logger.LoggerFactory;
import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.jwt.JwtAlgorithmProperties;
import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.jwt.JwtIdentityAccessManagementProperties;
import im.turms.server.common.infra.security.jwt.Jwt;
import im.turms.server.common.infra.security.jwt.JwtManager;
import im.turms.server.common.infra.security.jwt.JwtPayload;
import im.turms.server.common.infra.security.jwt.exception.InvalidJwtException;
import im.turms.server.common.infra.security.jwt.exception.JwtSignatureVerificationException;

import static im.turms.gateway.domain.session.bo.UserPermissionInfo.*;

/**
* @author James Chen
*/
public class JwtSessionIdentityAccessManager implements SessionIdentityAccessManagementSupport {

private static final Logger LOGGER =
LoggerFactory.getLogger(JwtSessionIdentityAccessManager.class);

private final JwtManager jwtManager;
private final PolicyManager policyManager;
private final Map<String, Object> jwtAuthenticationExpectedCustomPayloadClaims;

public JwtSessionIdentityAccessManager(
JwtIdentityAccessManagementProperties jwtProperties,
PolicyManager policyManager) {
jwtAuthenticationExpectedCustomPayloadClaims = jwtProperties.getAuthentication()
.getExpectation()
.getCustomPayloadClaims();
JwtAlgorithmProperties jwtAlgorithmProperties = jwtProperties.getAlgorithm();
jwtManager = new JwtManager(
jwtProperties.getVerification(),
jwtAlgorithmProperties.getRsa256(),
jwtAlgorithmProperties.getRsa384(),
jwtAlgorithmProperties.getRsa512(),
jwtAlgorithmProperties.getPs256(),
jwtAlgorithmProperties.getPs384(),
jwtAlgorithmProperties.getPs512(),
jwtAlgorithmProperties.getEcdsa256(),
jwtAlgorithmProperties.getEcdsa384(),
jwtAlgorithmProperties.getEcdsa512(),
jwtAlgorithmProperties.getHmac256(),
jwtAlgorithmProperties.getHmac384(),
jwtAlgorithmProperties.getHmac512());
LOGGER.info("Supported algorithms for JWT: {}", jwtManager.getSupportedAlgorithmNames());

this.policyManager = policyManager;
}

@Override
public Mono<UserPermissionInfo> verifyAndGrant(UserLoginInfo userLoginInfo) {
Long userId = userLoginInfo.userId();
String jwtToken = userLoginInfo.password();
if (StringUtil.isBlank(jwtToken)) {
return Mono.error(
new IllegalArgumentException("Invalid JWT token: JWT must not be blank"));
}
Jwt jwt;
try {
jwt = jwtManager.decode(jwtToken);
} catch (InvalidJwtException | NoSuchAlgorithmException
| JwtSignatureVerificationException e) {
return Mono.error(new IllegalArgumentException("Invalid JWT token", e));
}
JwtPayload payload = jwt.payload();
String subject = payload.subject();
if (subject == null) {
return Mono.error(new IllegalArgumentException(
"Invalid JWT token: the sub claim in the payload must exist"));
}
if (!subject.equals(userId.toString())) {
return LOGIN_AUTHENTICATION_FAILED_MONO;
}
Date expiresAt = payload.expiresAt();
Date notBefore = payload.notBefore();
boolean hasExpiresAt = expiresAt != null;
boolean hasNotBefore = notBefore != null;
if (hasExpiresAt || hasNotBefore) {
long now = System.currentTimeMillis();
if ((hasExpiresAt && expiresAt.getTime() <= now)
|| (hasNotBefore && notBefore.getTime() > now)) {
return LOGIN_AUTHENTICATION_FAILED_MONO;
}
}
Map<String, Object> customClaims = payload.customClaims();
if (!CollectionUtil.containsAllLooseComparison(customClaims,
jwtAuthenticationExpectedCustomPayloadClaims)) {
return LOGIN_AUTHENTICATION_FAILED_MONO;
}
Policy policy;
try {
policy = PolicyDeserializer.parse(customClaims);
} catch (IllegalPolicyException e) {
return Mono.error(new IllegalArgumentException("Invalid JWT token", e));
}
return Mono.just(new UserPermissionInfo(
ResponseStatusCode.OK,
policyManager.findAllowedRequestTypes(policy)));
}
}
Loading

0 comments on commit 6e26490

Please sign in to comment.