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

mrc-5092 Proof of concept: support basic auth for Montagu #53

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 3 additions & 1 deletion api/app/src/main/kotlin/packit/AppConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ class AppConfig(private val props: PackitProperties = properties)
val dbUrl: String = propString("db.url")
val dbUser: String = propString("db.user")
val dbPassword: String = propString("db.password")
val appRoute: String = propString("app.route")
val authJWTSecret: String = propString("auth.jwt.secret")
val authRedirectUri: String = propString("auth.oauth2.redirect.url")
val authEnableGithubLogin: Boolean = propString("auth.enableGithubLogin").toBoolean()
val authEnableGithubLogin: Boolean = propString("auth.method") == "github"
val authEnableBasicLogin: Boolean = propString("auth.method") == "basic"
val authExpiryDays: Long = propString("auth.expiryDays").toLong()
val authEnabled: Boolean = propString("auth.enabled").toBoolean()
val authGithubAPIOrg: String = propString("auth.githubAPIOrg")
Expand Down
1 change: 1 addition & 0 deletions api/app/src/main/kotlin/packit/CorsConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class CorsConfig
config.allowCredentials = true
config.addAllowedOrigin("http://localhost:3000")
config.addAllowedOrigin("http://localhost")
config.addAllowedOrigin("https://localhost") // TODO: Pull from config?
config.addAllowedHeader("*")
config.addAllowedMethod("*")
source.registerCorsConfiguration("/**", config)
Expand Down
12 changes: 4 additions & 8 deletions api/app/src/main/kotlin/packit/clients/GithubUserClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package packit.clients
import org.kohsuke.github.*
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.security.core.authority.AuthorityUtils
import packit.AppConfig
import packit.exceptions.PackitAuthenticationException
import packit.model.User
import packit.security.Role
import packit.security.profile.UserPrincipal

Expand All @@ -21,17 +21,13 @@ class GithubUserClient(private val config: AppConfig, private val githubBuilder:
ghUser = getGitHubUser()
}

fun getUser(): UserPrincipal
fun getUserPrincipal(): UserPrincipal
{
checkAuthenticated()
val ghu = ghUser!!

val user = User(
ghu.login,
ghu.name,
listOf(Role.USER)
)
return UserPrincipal.create(user, mutableMapOf())
val authorities = AuthorityUtils.createAuthorityList(listOf(Role.USER).toString())
return UserPrincipal(ghu.login, ghu.name, authorities, mutableMapOf())
}

@Throws(PackitAuthenticationException::class)
Expand Down
21 changes: 20 additions & 1 deletion api/app/src/main/kotlin/packit/controllers/LoginController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import org.springframework.http.ResponseEntity
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
import packit.AppConfig
import packit.model.LoginWithPassword
import packit.model.LoginWithToken
import packit.service.BasicLoginService
import packit.service.GithubAPILoginService

@RestController
@RequestMapping("/auth")
class LoginController(
val gitApiLoginService: GithubAPILoginService,
val basicLoginService: BasicLoginService,
val config: AppConfig
)
{
Expand All @@ -24,13 +27,29 @@ class LoginController(
return ResponseEntity.ok(token)
}

@PostMapping("/login/basic")
@ResponseBody
fun loginBasic(
@RequestBody @Validated user: LoginWithPassword
): ResponseEntity<Map<String, String>>
{
// TODO: Error if basic auth not supported. Should do the same for github api login
val token = basicLoginService.authenticateAndIssueToken(user)
return ResponseEntity.ok(token)
}

@GetMapping("/config")
@ResponseBody
fun authConfig(): ResponseEntity<Map<String, Any>>
{
// TODO: 'appRoute' is needed by the front end to prepend routes, which may be needed if Packit is to be deployed
// under a route within the domain (e.g. /packit in the case of Montagu deploy). It isn't *really* auth config
// as such (though related), so consider if this is the right place to return it
val authConfig = mapOf(
"enableGithubLogin" to config.authEnableGithubLogin,
"enableAuth" to config.authEnabled
"enableBasicLogin" to config.authEnableBasicLogin,
"enableAuth" to config.authEnabled,
"appRoute" to config.appRoute
)
return ResponseEntity.ok(authConfig)
}
Expand Down
11 changes: 11 additions & 0 deletions api/app/src/main/kotlin/packit/model/BasicUser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package packit.model

import packit.security.Role

data class BasicUser(
val userName: String,
val password: String,
val displayName: String?,
val role: List<Role>,
val attributes: MutableMap<String, Any>
)
5 changes: 5 additions & 0 deletions api/app/src/main/kotlin/packit/model/LoginWithPassword.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package packit.model

import org.jetbrains.annotations.NotNull

data class LoginWithPassword(@NotNull val email: String, @NotNull val password: String)
9 changes: 0 additions & 9 deletions api/app/src/main/kotlin/packit/model/User.kt

This file was deleted.

14 changes: 14 additions & 0 deletions api/app/src/main/kotlin/packit/security/WebSecurityConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package packit.security
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpStatus
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.config.Customizer.withDefaults
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
Expand All @@ -16,11 +18,13 @@ import packit.AppConfig
import packit.exceptions.PackitExceptionHandler
import packit.security.oauth2.*
import packit.security.provider.JwtIssuer
import packit.service.BasicUserDetailsService

@EnableWebSecurity
@Configuration
class WebSecurityConfig(
val customOauth2UserService: OAuth2UserService,
val customBasicUserService: BasicUserDetailsService,
val config: AppConfig,
val jwtIssuer: JwtIssuer,
val browserRedirect: BrowserRedirect,
Expand Down Expand Up @@ -90,4 +94,14 @@ class WebSecurityConfig(
{
return BCryptPasswordEncoder()
}

@Bean
fun authenticationManager(httpSecurity: HttpSecurity): AuthenticationManager
{
return httpSecurity.getSharedObject(AuthenticationManagerBuilder::class.java)
.userDetailsService(customBasicUserService)
.passwordEncoder(passwordEncoder())
.and()
.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.springframework.stereotype.Component
import org.springframework.util.LinkedMultiValueMap
import packit.security.BrowserRedirect
import packit.security.provider.JwtIssuer
import packit.security.profile.PackitOAuth2User

@Component
class OAuth2SuccessHandler(
Expand All @@ -31,7 +32,8 @@ class OAuth2SuccessHandler(
authentication: Authentication,
)
{
val token = jwtIssuer.issue(authentication)
val user = authentication.principal as PackitOAuth2User
val token = jwtIssuer.issue(user.principal)

val queryString = LinkedMultiValueMap<String, String>().apply{ this.add("token", token) }
redirect.redirectToBrowser(request, response, queryString)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import org.springframework.security.oauth2.core.user.OAuth2User
import org.springframework.stereotype.Component
import packit.clients.GithubUserClient
import packit.exceptions.PackitException
import packit.model.User
import packit.security.Role
import packit.security.profile.PackitOAuth2User
import packit.security.profile.UserPrincipal
import org.springframework.security.core.authority.AuthorityUtils

@Component
class OAuth2UserService(private val githubUserClient: GithubUserClient) : DefaultOAuth2UserService()
Expand All @@ -33,13 +34,13 @@ class OAuth2UserService(private val githubUserClient: GithubUserClient) : Defaul

// TODO check if user exists, if not, save user email to database

val user = User(
val principal = UserPrincipal(
githubInfo.userName(),
githubInfo.displayName(),
listOf(Role.USER)
AuthorityUtils.createAuthorityList(listOf(Role.USER).toString()),
oAuth2User.attributes
)

return UserPrincipal.create(user, oAuth2User.attributes)
return PackitOAuth2User(principal)
}

fun checkGithubUserMembership(request: OAuth2UserRequest)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package packit.security.profile

import net.minidev.json.annotate.JsonIgnore
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.authority.AuthorityUtils

class BasicUserDetails(
val principal: UserPrincipal,
@JsonIgnore
private val password: String
) : UserDetails
{
override fun getAuthorities(): MutableCollection<out GrantedAuthority>
{
return principal.authorities
}

override fun getPassword(): String
{
return password
}

override fun getUsername(): String
{
return principal.name
}

override fun isAccountNonExpired(): Boolean
{
return true
}

override fun isAccountNonLocked(): Boolean
{
return true
}

override fun isCredentialsNonExpired(): Boolean
{
return true
}

override fun isEnabled(): Boolean
{
return true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package packit.security.profile

import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.AuthorityUtils
import org.springframework.security.oauth2.core.user.OAuth2User

class PackitOAuth2User(
val principal: UserPrincipal
) : OAuth2User
{
override fun getAttributes(): MutableMap<String, Any>
{
return principal.attributes
}

override fun getAuthorities(): MutableCollection<out GrantedAuthority>
{
return principal.authorities
}

override fun getName(): String
{
return principal.name
}
}
43 changes: 5 additions & 38 deletions api/app/src/main/kotlin/packit/security/profile/UserPrincipal.kt
Original file line number Diff line number Diff line change
@@ -1,43 +1,10 @@
package packit.security.profile

import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.AuthorityUtils
import org.springframework.security.oauth2.core.user.OAuth2User
import packit.model.User

open class UserPrincipal(
private val name: String,
data class UserPrincipal(
val name: String,
val displayName: String?,
private val authorities: MutableCollection<out GrantedAuthority>,
private val attributes: MutableMap<String, Any>,
) : OAuth2User
{
companion object
{
fun create(user: User, attributes: MutableMap<String, Any>): UserPrincipal
{
val authorities = AuthorityUtils.createAuthorityList(user.role.toString())
return UserPrincipal(
user.userName,
user.displayName,
authorities,
attributes
)
}
}

override fun getAttributes(): MutableMap<String, Any>
{
return attributes
}

override fun getAuthorities(): MutableCollection<out GrantedAuthority>
{
return authorities
}

override fun getName(): String
{
return name
}
}
val authorities: MutableCollection<out GrantedAuthority>,
val attributes: MutableMap<String, Any>
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import java.time.temporal.ChronoUnit

interface JwtIssuer
{
fun issue(authentication: Authentication): String
fun issue(user: UserPrincipal): String
}

@Component
Expand All @@ -24,10 +24,8 @@ class TokenProvider(val config: AppConfig) : JwtIssuer
const val TOKEN_AUDIENCE = "packit"
}

override fun issue(authentication: Authentication): String
override fun issue(user: UserPrincipal): String
{
val user = authentication.principal as UserPrincipal

val roles = user.authorities.map { it.authority }

val createdDate = Instant.now()
Expand Down
Loading
Loading