Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#60 displayname added to JWT #61

Merged
merged 3 commits into from
Aug 21, 2023
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
1 change: 1 addition & 0 deletions service/src/main/resources/example.application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ loginsvc:
groups: []
- username: "TestUser"
password: "password123"
displayname: "Test User, A.C.E."
email: "[email protected]"
groups:
- "groupA"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@

package za.co.absa.loginsvc.model

case class User(name: String, email: Option[String], groups: Seq[String])
case class User(name: String, email: Option[String], displayName: Option[String], groups: Seq[String])
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@ case class UsersConfig(knownUsers: Array[UserConfig], order: Int)
case class UserConfig(username: String,
password: String,
email: Option[String],
displayname: Option[String],
groups: Array[String]
) extends ConfigValidatable {

override def toString: String = {
s"UserConfig($username, $password, ${email.getOrElse("")}, ${Option(groups).map(_.toList)})"
s"UserConfig($username, $password, ${email.getOrElse("")}, ${displayname.getOrElse("")}, ${Option(groups).map(_.toList)})"
}

override def validate(): ConfigValidationResult = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class ConfigUsersAuthenticationProvider(usersConfig: UsersConfig) extends Authen
usersConfig.knownUsersMap.get(username).map { usersConfig =>
if (usersConfig.password == password) {
logger.info(s"user login: $username - ok")
val principal = User(username, usersConfig.email, usersConfig.groups.toList)
val principal = User(username, usersConfig.email, usersConfig.displayname, usersConfig.groups.toList)
new UsernamePasswordAuthenticationToken(principal, password,
usersConfig.groups.map(new SimpleGrantedAuthority(_)).toList.asJava)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ class ActiveDirectoryLDAPAuthenticationProvider(config: ActiveDirectoryLDAPConfi

override def authenticate(authentication: Authentication): Authentication = {
val fromBase = baseImplementation.authenticate(authentication)
val fromBasePrincipal = fromBase.getPrincipal.asInstanceOf[UserDetailsWithEmail]
val fromBasePrincipal = fromBase.getPrincipal.asInstanceOf[UserDetailsWithExtras]

val principal = User(
fromBasePrincipal.getUsername,
fromBasePrincipal.email,
fromBasePrincipal.displayName,
fromBasePrincipal.getAuthorities.asScala.map(_.getAuthority).toSeq
)

Expand All @@ -60,7 +61,7 @@ class ActiveDirectoryLDAPAuthenticationProvider(config: ActiveDirectoryLDAPConfi
override def supports(authentication: Class[_]): Boolean = baseImplementation.supports(authentication)


private case class UserDetailsWithEmail(userDetails: UserDetails, email: Option[String]) extends UserDetails {
private case class UserDetailsWithExtras(userDetails: UserDetails, email: Option[String], displayName: Option[String]) extends UserDetails {
override def getAuthorities: util.Collection[_ <: GrantedAuthority] = userDetails.getAuthorities
override def getPassword: String = userDetails.getPassword
override def getUsername: String = userDetails.getUsername
Expand All @@ -79,7 +80,9 @@ class ActiveDirectoryLDAPAuthenticationProvider(config: ActiveDirectoryLDAPConfi
): UserDetails = {
val fromBase = super.mapUserFromContext(ctx, username, authorities)
val email = Option(ctx.getAttributes().get("mail")).map(_.get().toString)
UserDetailsWithEmail(fromBase, email)
val displayName = Option(ctx.getAttributes().get("displayname")).map(_.get().toString)

UserDetailsWithExtras(fromBase, email, displayName)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
package za.co.absa.loginsvc.rest.service

import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.{Jwts, SignatureAlgorithm}
import io.jsonwebtoken.{JwtBuilder, Jwts, SignatureAlgorithm}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import za.co.absa.loginsvc.model.User
import za.co.absa.loginsvc.rest.config.provider.JwtConfigProvider
import za.co.absa.loginsvc.rest.service.JWTService.JwtBuilderExt
import za.co.absa.loginsvc.utils.OptionExt

import java.security.{KeyPair, PublicKey}
import java.time.Instant
Expand All @@ -45,21 +47,26 @@ class JWTService @Autowired()(jwtConfigProvider: JwtConfigProvider) {
// needs to be Java List/Array, otherwise incorrect claim is generated
val groupsClaim = user.groups.asJava

val jwtBuilderWithoutEmail = Jwts
Jwts
.builder()
.setSubject(user.name)
.setExpiration(expiration)
.setIssuedAt(issuedAt)
.claim("groups", groupsClaim)

user
.email
.map(jwtBuilderWithoutEmail.claim("email", _))
.getOrElse(jwtBuilderWithoutEmail)
.applyIfDefined(user.email, (builder, value: String) => builder.claim("email", value))
.applyIfDefined(user.displayName, (builder, value: String) => builder.claim("displayname", value))
.signWith(rsaKeyPair.getPrivate)
.compact()
}

def publicKey: PublicKey = rsaKeyPair.getPublic

}

object JWTService {
implicit class JwtBuilderExt(val jwtBuilder: JwtBuilder) extends AnyVal {
def applyIfDefined[T](opt: Option[T], fn: (JwtBuilder, T) => JwtBuilder): JwtBuilder = {
OptionExt.applyIfDefined(jwtBuilder, opt, fn)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2023 ABSA Group Limited
*
* 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 za.co.absa.loginsvc.utils

object OptionExt {

/**
* For `target`, either return as is (if `optValueToApply` is None) or apply fn `func`
* @param target
* @param optValueToApply
* @param func
* @tparam A
* @tparam B
* @return
*/
def applyIfDefined[A, B](target: A, optValueToApply: Option[B], func: (A, B) => A): A = {
optValueToApply.map { valueToApply =>
func(target, valueToApply)
}.getOrElse {
target
}
}

}
6 changes: 6 additions & 0 deletions service/src/test/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ loginsvc:
password: "password1"
groups:
- "group1"
- username: "user2"
password: "password2"
displayname: "User Two"
email: "[email protected]"
groups:
- "group2"

# App Config
spring:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import java.util

object FakeAuthentication {

val fakeUser: User = User("fakeUser", Some("[email protected]"), Seq.empty)
val fakeUser: User = User("fakeUser", Some("[email protected]"), Some("Fake Name"), Seq.empty)

val fakeUserAuthentication: Authentication = new UsernamePasswordAuthenticationToken(
fakeUser, "fakePassword", new util.ArrayList[GrantedAuthority]()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import za.co.absa.loginsvc.rest.config.validation.ConfigValidationResult.{Config

class UsersConfigTest extends AnyFlatSpec with Matchers {

private val userCfg = UserConfig("user1", "password1", Option("[email protected]"), Array("group1", "group2"))
private val userCfg = UserConfig("user1", "password1", Option("[email protected]"), Option("Fake Name"), Array("group1", "group2"))

"UserConfig" should "validate expected filled content" in {
userCfg.validate() shouldBe ConfigValidationSuccess
Expand Down Expand Up @@ -67,13 +67,13 @@ class UsersConfigTest extends AnyFlatSpec with Matchers {

it should "fail on duplicate knownUsers" in {
val duplicateValidationResult = UsersConfig(knownUsers = Array(
UserConfig("sameUser", "password1", Option("[email protected]"), Array("group1", "group2")),
UserConfig("sameUser", "password2", Option("[email protected]"), Array()),
UserConfig("sameUser", "password1", Option("[email protected]"), Option("Fake1"), Array("group1", "group2")),
UserConfig("sameUser", "password2", Option("[email protected]"), Option("Fake2"), Array()),

UserConfig("sameUser2", "passwordX", Option("abc@def"), Array()),
UserConfig("sameUser2", "passwordA", Option(null), Array()),
UserConfig("sameUser2", "passwordX", Option("abc@def"), Option("Fake1"), Array()),
UserConfig("sameUser2", "passwordA", Option(null), Option(null), Array()),

UserConfig("okUser", "passwordO", Option("ooo@"), Array())
UserConfig("okUser", "passwordO", Option("ooo@"), Option("Fake1"), Array())
), 1).validate()

duplicateValidationResult shouldBe a[ConfigValidationError]
Expand All @@ -86,12 +86,12 @@ class UsersConfigTest extends AnyFlatSpec with Matchers {

it should "fail multiple errors" in {
val multiErrorsResult = UsersConfig(knownUsers = Array(
UserConfig("sameUser", "password1", Option("[email protected]"), Array("group1", "group2")),
UserConfig("sameUser", "password2", Option("[email protected]"), Array()),
UserConfig("sameUser", "password1", Option("[email protected]"), Option("Fake1"), Array("group1", "group2")),
UserConfig("sameUser", "password2", Option("[email protected]"), Option("Fake2"), Array()),

UserConfig("userNoPass", null, Option("abc@def"), Array()),
UserConfig("noMailIsFine", "password2", Option(null), Array()),
UserConfig("userNoMissingGroups", "passwordO", Option("ooo@"), null)
UserConfig("userNoPass", null, Option("abc@def"), Option("FakeName"), Array()),
UserConfig("noMailIsFine", "password2", Option(null), Option(null), Array()),
UserConfig("userNoMissingGroups", "passwordO", Option("ooo@"), Option("Fake2"), null)
), 1).validate()

multiErrorsResult shouldBe a[ConfigValidationError]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,20 @@ class ConfigProviderTest extends AnyFlatSpec with Matchers {
activeDirectoryLDAPConfig.order == 1)
}

"The usersConfig properties" should "Match" in {
"The usersConfig properties" should "be loaded correctly" in {
val usersConfig: UsersConfig = configProvider.getUsersConfig
assert(usersConfig.order == 0)

assert(usersConfig.knownUsers(0).groups(0) == "group1" &&
usersConfig.knownUsers(0).email.isEmpty &&
usersConfig.knownUsers(0).displayname.isEmpty &&
usersConfig.knownUsers(0).password == "password1" &&
usersConfig.knownUsers(0).username == "user1" &&
usersConfig.order == 0)
usersConfig.knownUsers(0).username == "user1")

assert(usersConfig.knownUsers(1).groups(0) == "group2" &&
usersConfig.knownUsers(1).email == Some("[email protected]") &&
usersConfig.knownUsers(1).displayname == Some("User Two") &&
usersConfig.knownUsers(1).password == "password2" &&
usersConfig.knownUsers(1).username == "user2")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class ConfigUsersAuthenticationProviderTest extends AnyFlatSpec with Matchers {

// testing config we are running against
val testConfig: UsersConfig = UsersConfig(Array(
UserConfig("testuser", "testpassword", Option("[email protected]"), Array())
UserConfig("testuser", "testpassword", Option("[email protected]"), Option("Test User"), Array())
), 1)
val authProvider = new ConfigUsersAuthenticationProvider(testConfig)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class JWTServiceTest extends AnyFlatSpec {
private val userWithoutEmailAndGroups: User = User(
name = "testUser",
email = None,
displayName = None,
groups = Seq.empty
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2023 ABSA Group Limited
*
* 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 za.co.absa.loginsvc.utils

import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class OptionExtTest extends AnyFlatSpec with Matchers {

"OptionExt.applyIfDefined" should "apply fn correctly if defined" in {
OptionExt.applyIfDefined(1, Some(2), (a:Int, b: Int) => a + b) shouldBe 3
}

"OptionExt.applyIfDefined" should "not apply fn if empty" in {
OptionExt.applyIfDefined(1, None, (a: Int, b: Int) => a + b) shouldBe 1
}

}
Loading