make auth work

This commit is contained in:
gowthaman.b 2023-11-10 14:05:58 +05:30
parent b0042d2e6b
commit 1e3d9aa1f1
9 changed files with 352 additions and 16 deletions

View File

@ -1,17 +1,19 @@
### create row ### create row
POST http://localhost:9001/api/vehicle POST http://localhost:9001/api/vehicle
Content-Type: application/json Content-Type: application/json
Authorization: set-auth-token
{ {
"data": { "data": {
"number": "KA03HD6064" "number": "KA01MU0556"
}, },
"uniqueIdentifier": "KA03HD6064" "uniqueIdentifier": "KA01MU0556"
} }
### create row, with autogenerated identifier ### create row, with autogenerated identifier
POST http://localhost:9001/api/log POST http://localhost:9001/api/log
Content-Type: application/json Content-Type: application/json
Authorization: set-auth-token
{ {
"data": { "data": {
@ -26,6 +28,7 @@ GET http://localhost:9001/api/vehicle/KA03HD6064
### query row ### query row
POST http://localhost:9001/api/vehicle/query POST http://localhost:9001/api/vehicle/query
Content-Type: application/json Content-Type: application/json
Authorization: set-auth-token
{ {
"sql": "select sys_pk, tenant_id, deleted_on, deleted_by, deleted, version, created_at, modified_at, created_by, modified_by, data, tags, comments, unique_identifier, entity_name from data_model where data ->> 'number' = :number", "sql": "select sys_pk, tenant_id, deleted_on, deleted_by, deleted, version, created_at, modified_at, created_by, modified_by, data, tags, comments, unique_identifier, entity_name from data_model where data ->> 'number' = :number",
@ -37,6 +40,7 @@ Content-Type: application/json
### update field ### update field
PATCH http://localhost:9001/api/vehicle/KA03HD6064 PATCH http://localhost:9001/api/vehicle/KA03HD6064
Content-Type: application/json Content-Type: application/json
Authorization: set-auth-token
{ {
"key": "ownerName", "key": "ownerName",
@ -47,6 +51,7 @@ Content-Type: application/json
### upate a row ### upate a row
PUT http://localhost:9001/api/vehicle/KA03HD6064 PUT http://localhost:9001/api/vehicle/KA03HD6064
Content-Type: application/json Content-Type: application/json
Authorization: set-auth-token
{ {
"number": "KA03HD6064", "number": "KA03HD6064",

View File

@ -4,4 +4,9 @@ app.cors.hosts=www.readymixerp.com,app.readymixerp.com
app.db.user=postgres app.db.user=postgres
app.db.pass=postgres app.db.pass=postgres
app.db.url=jdbc:postgresql://192.168.64.6/modules_app app.db.url=jdbc:postgresql://192.168.64.6/modules_app
app.db.run_migration=true app.db.run_migration=true
app.iam.url=https://auth.compegence.com
app.iam.realm=forewarn-dev
app.iam.client=forewarn
app.iam.client_redirect_uri=http://localhost:9001/auth/code
app.cache.redis_uri=redis://127.0.0.1:6379/0

View File

@ -26,7 +26,9 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.+") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.+")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.+") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.+")
implementation("org.bitbucket.b_c:jose4j:0.9.3")
implementation("org.slf4j:slf4j-simple:2.0.7") implementation("org.slf4j:slf4j-simple:2.0.7")
implementation("redis.clients:jedis:5.0.2")
api ("net.cactusthorn.config:config-core:0.81") api ("net.cactusthorn.config:config-core:0.81")
kapt("net.cactusthorn.config:config-compiler:0.81") kapt("net.cactusthorn.config:config-compiler:0.81")
kapt("io.ebean:kotlin-querybean-generator:13.23.2") kapt("io.ebean:kotlin-querybean-generator:13.23.2")

View File

@ -1,7 +1,12 @@
package com.restapi package com.restapi
import AuthTokenResponse
import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.module.kotlin.readValue
import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.config.Auth
import com.restapi.config.Auth.getAuthEndpoint
import com.restapi.config.AuthEndpoint
import com.restapi.domain.DataModel import com.restapi.domain.DataModel
import com.restapi.domain.DataNotFoundException import com.restapi.domain.DataNotFoundException
import com.restapi.domain.Session import com.restapi.domain.Session
@ -9,6 +14,9 @@ import com.restapi.domain.Session.creatSeq
import com.restapi.domain.Session.database import com.restapi.domain.Session.database
import com.restapi.domain.Session.findByEntityAndId import com.restapi.domain.Session.findByEntityAndId
import com.restapi.domain.Session.nextUniqId import com.restapi.domain.Session.nextUniqId
import com.restapi.domain.Session.objectMapper
import com.restapi.domain.Session.redis
import com.restapi.domain.Session.setAuthorizedUser
import io.ebean.CallableSql import io.ebean.CallableSql
import io.ebean.DuplicateKeyException import io.ebean.DuplicateKeyException
import io.ebean.RawSqlBuilder import io.ebean.RawSqlBuilder
@ -17,10 +25,18 @@ import io.javalin.apibuilder.ApiBuilder.*
import io.javalin.http.* import io.javalin.http.*
import io.javalin.json.JavalinJackson import io.javalin.json.JavalinJackson
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.net.URI
import java.net.URLEncoder
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpRequest.BodyPublishers
import java.net.http.HttpResponse.BodyHandlers
import java.nio.charset.StandardCharsets
import java.time.LocalDateTime import java.time.LocalDateTime
fun main(args: Array<String>) { fun main(args: Array<String>) {
val logger = LoggerFactory.getLogger("api") val logger = LoggerFactory.getLogger("api")
Javalin Javalin
.create { cfg -> .create { cfg ->
cfg.http.generateEtags = true cfg.http.generateEtags = true
@ -39,7 +55,47 @@ fun main(args: Array<String>) {
cfg.jsonMapper(JavalinJackson(Session.objectMapper)) cfg.jsonMapper(JavalinJackson(Session.objectMapper))
} }
.routes { .routes {
before("/*") { ctx ->
path("/auth") {
get("/init") {
val endpoint = getAuthEndpoint().authorizationEndpoint
val redirectUrl =
"$endpoint?response_type=code&client_id=${appConfig.iamClient()}&redirect_uri=${appConfig.iamClientRedirectUri()}&scope=profile&state=1234zyx"
it.redirect(redirectUrl)
}
get("/code") {
val code = it.queryParam("code") ?: throw BadRequestResponse("not proper")
val ep = getAuthEndpoint().tokenEndpoint
val client = HttpClient.newHttpClient()
val req = HttpRequest.newBuilder()
.uri(URI.create(ep))
.POST(
BodyPublishers.ofString(
getFormDataAsString(
mapOf(
"code" to code,
"redirect_uri" to appConfig.iamClientRedirectUri(),
"client_id" to appConfig.iamClient(),
"grant_type" to "authorization_code",
)
)
)
)
.header("Content-Type", "application/x-www-form-urlencoded")
.build()
val message = client.send(req, BodyHandlers.ofString()).body()
val atResponse = objectMapper.readValue<AuthTokenResponse>(message)
//lets keep auth token refreshed
redis.sadd("AUTH_TOKEN", message)
it.result(atResponse.accessToken).contentType(ContentType.TEXT_PLAIN)
}
}
before("/api/*") { ctx ->
//validate, auth token //validate, auth token
//allow only alpha, numeric, hypen, underscore, dot in paths //allow only alpha, numeric, hypen, underscore, dot in paths
@ -51,6 +107,12 @@ fun main(args: Array<String>) {
throw IllegalArgumentException() throw IllegalArgumentException()
} }
} }
val at = ctx.header("Authorization")?.replace("Bearer ", "")?.replace("Bearer: ", "")?.trim()
?: throw UnauthorizedResponse()
val pt = Auth.parseAuthToken(authToken = at)
setAuthorizedUser(pt)
} }
path("/api") { path("/api") {
post("/execute/{name}") { post("/execute/{name}") {
@ -174,4 +236,17 @@ data class Query(
val params: Map<String, Any> val params: Map<String, Any>
) )
private fun getFormDataAsString(formData: Map<String, String>): String {
val formBodyBuilder = StringBuilder()
for ((key, value) in formData) {
if (formBodyBuilder.length > 0) {
formBodyBuilder.append("&")
}
formBodyBuilder.append(URLEncoder.encode(key, StandardCharsets.UTF_8))
formBodyBuilder.append("=")
formBodyBuilder.append(URLEncoder.encode(value, StandardCharsets.UTF_8))
}
return formBodyBuilder.toString()
}
data class PatchValue(val key: String, val value: Any) data class PatchValue(val key: String, val value: Any)

View File

@ -5,6 +5,7 @@ import net.cactusthorn.config.core.Default
import net.cactusthorn.config.core.Key import net.cactusthorn.config.core.Key
import net.cactusthorn.config.core.factory.ConfigFactory import net.cactusthorn.config.core.factory.ConfigFactory
import net.cactusthorn.config.core.loader.LoadStrategy import net.cactusthorn.config.core.loader.LoadStrategy
import java.util.Optional
const val INITIAL_ROLES_JSON = """{ const val INITIAL_ROLES_JSON = """{
"roles": [] "roles": []
@ -41,6 +42,26 @@ interface AppConfig {
@Key("app.db.run_migration") @Key("app.db.run_migration")
fun dbRunMigration(): Boolean fun dbRunMigration(): Boolean
@Key("app.iam.url")
fun iamUrl(): String
@Key("app.iam.realm")
fun iamRealm(): String
@Key("app.iam.client")
fun iamClient(): String
@Key("app.iam.client_redirect_uri")
fun iamClientRedirectUri(): String
@Key("app.iam.client_id")
fun iamClientId(): Optional<String>
@Key("app.iam.client_secret")
fun iamClientSecret(): Optional<String>
@Key("app.cache.redis_uri")
fun redisUri(): Optional<String>
companion object { companion object {
val appConfig: AppConfig = ConfigFactory.builder().build().create(AppConfig::class.java) val appConfig: AppConfig = ConfigFactory.builder().build().create(AppConfig::class.java)

View File

@ -0,0 +1,72 @@
package com.restapi.config
import com.fasterxml.jackson.module.kotlin.readValue
import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.domain.Session
import com.restapi.domain.Session.objectMapper
import io.javalin.http.Context
import io.javalin.http.HandlerType
import io.javalin.http.UnauthorizedResponse
import org.jose4j.jwk.HttpsJwks
import org.jose4j.jwt.consumer.ErrorCodes
import org.jose4j.jwt.consumer.InvalidJwtException
import org.jose4j.jwt.consumer.JwtConsumerBuilder
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver
import org.slf4j.LoggerFactory
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Function
object Auth {
private val authCache = ConcurrentHashMap<String, AuthEndpoint>()
fun getAuthEndpoint(): AuthEndpoint {
return authCache.computeIfAbsent("AUTH") {
val wellKnown =
"${appConfig.iamUrl()}/realms/${appConfig.iamRealm()}/.well-known/openid-configuration"
val client = HttpClient.newHttpClient()
val req = HttpRequest.newBuilder()
.uri(URI.create(wellKnown))
.GET().build()
objectMapper.readValue<AuthEndpoint>(
client.send(req, HttpResponse.BodyHandlers.ofString()).body()
)
}
}
private val jwtConsumer = JwtConsumerBuilder()
.setRequireExpirationTime()
.setAllowedClockSkewInSeconds(30)
.setRequireSubject()
.setExpectedIssuer(getAuthEndpoint().issuer)
.setExpectedAudience("account")
.setVerificationKeyResolver(HttpsJwksVerificationKeyResolver(HttpsJwks(getAuthEndpoint().jwksUri)))
.build()
fun parseAuthToken(authToken: String): AuthUser {
// Validate the JWT and process it to the Claims
val jwtClaims = jwtConsumer.process(authToken)
val userId = jwtClaims.jwtClaims.claimsMap["preferred_username"] as String
val tenant = jwtClaims.jwtClaims.claimsMap["tenant"] as String
val roles = ((jwtClaims.jwtClaims.claimsMap["realm_access"] as Map<String, Any>)["roles"]) as List<String>
return AuthUser(
userName = userId,
tenant = tenant,
roles = roles
)
}
}
data class AuthUser(val userName: String, val tenant: String, val roles: List<String>)

View File

@ -0,0 +1,132 @@
package com.restapi.config
import com.fasterxml.jackson.annotation.JsonProperty
data class AuthEndpoint(
@JsonProperty("acr_values_supported")
val acrValuesSupported: List<String>,
@JsonProperty("authorization_encryption_alg_values_supported")
val authorizationEncryptionAlgValuesSupported: List<String>,
@JsonProperty("authorization_encryption_enc_values_supported")
val authorizationEncryptionEncValuesSupported: List<String>,
@JsonProperty("authorization_endpoint")
val authorizationEndpoint: String,
@JsonProperty("authorization_signing_alg_values_supported")
val authorizationSigningAlgValuesSupported: List<String>,
@JsonProperty("backchannel_authentication_endpoint")
val backchannelAuthenticationEndpoint: String,
@JsonProperty("backchannel_authentication_request_signing_alg_values_supported")
val backchannelAuthenticationRequestSigningAlgValuesSupported: List<String>,
@JsonProperty("backchannel_logout_session_supported")
val backchannelLogoutSessionSupported: Boolean,
@JsonProperty("backchannel_logout_supported")
val backchannelLogoutSupported: Boolean,
@JsonProperty("backchannel_token_delivery_modes_supported")
val backchannelTokenDeliveryModesSupported: List<String>,
@JsonProperty("check_session_iframe")
val checkSessionIframe: String,
@JsonProperty("claim_types_supported")
val claimTypesSupported: List<String>,
@JsonProperty("claims_parameter_supported")
val claimsParameterSupported: Boolean,
@JsonProperty("claims_supported")
val claimsSupported: List<String>,
@JsonProperty("code_challenge_methods_supported")
val codeChallengeMethodsSupported: List<String>,
@JsonProperty("device_authorization_endpoint")
val deviceAuthorizationEndpoint: String,
@JsonProperty("end_session_endpoint")
val endSessionEndpoint: String,
@JsonProperty("frontchannel_logout_session_supported")
val frontchannelLogoutSessionSupported: Boolean,
@JsonProperty("frontchannel_logout_supported")
val frontchannelLogoutSupported: Boolean,
@JsonProperty("grant_types_supported")
val grantTypesSupported: List<String>,
@JsonProperty("id_token_encryption_alg_values_supported")
val idTokenEncryptionAlgValuesSupported: List<String>,
@JsonProperty("id_token_encryption_enc_values_supported")
val idTokenEncryptionEncValuesSupported: List<String>,
@JsonProperty("id_token_signing_alg_values_supported")
val idTokenSigningAlgValuesSupported: List<String>,
@JsonProperty("introspection_endpoint")
val introspectionEndpoint: String,
@JsonProperty("introspection_endpoint_auth_methods_supported")
val introspectionEndpointAuthMethodsSupported: List<String>,
@JsonProperty("introspection_endpoint_auth_signing_alg_values_supported")
val introspectionEndpointAuthSigningAlgValuesSupported: List<String>,
@JsonProperty("issuer")
val issuer: String,
@JsonProperty("jwks_uri")
val jwksUri: String,
@JsonProperty("mtls_endpoint_aliases")
val mtlsEndpointAliases: MtlsEndpointAliases,
@JsonProperty("pushed_authorization_request_endpoint")
val pushedAuthorizationRequestEndpoint: String,
@JsonProperty("registration_endpoint")
val registrationEndpoint: String,
@JsonProperty("request_object_encryption_alg_values_supported")
val requestObjectEncryptionAlgValuesSupported: List<String>,
@JsonProperty("request_object_encryption_enc_values_supported")
val requestObjectEncryptionEncValuesSupported: List<String>,
@JsonProperty("request_object_signing_alg_values_supported")
val requestObjectSigningAlgValuesSupported: List<String>,
@JsonProperty("request_parameter_supported")
val requestParameterSupported: Boolean,
@JsonProperty("request_uri_parameter_supported")
val requestUriParameterSupported: Boolean,
@JsonProperty("require_pushed_authorization_requests")
val requirePushedAuthorizationRequests: Boolean,
@JsonProperty("require_request_uri_registration")
val requireRequestUriRegistration: Boolean,
@JsonProperty("response_modes_supported")
val responseModesSupported: List<String>,
@JsonProperty("response_types_supported")
val responseTypesSupported: List<String>,
@JsonProperty("revocation_endpoint")
val revocationEndpoint: String,
@JsonProperty("revocation_endpoint_auth_methods_supported")
val revocationEndpointAuthMethodsSupported: List<String>,
@JsonProperty("revocation_endpoint_auth_signing_alg_values_supported")
val revocationEndpointAuthSigningAlgValuesSupported: List<String>,
@JsonProperty("scopes_supported")
val scopesSupported: List<String>,
@JsonProperty("subject_types_supported")
val subjectTypesSupported: List<String>,
@JsonProperty("tls_client_certificate_bound_access_tokens")
val tlsClientCertificateBoundAccessTokens: Boolean,
@JsonProperty("token_endpoint")
val tokenEndpoint: String,
@JsonProperty("token_endpoint_auth_methods_supported")
val tokenEndpointAuthMethodsSupported: List<String>,
@JsonProperty("token_endpoint_auth_signing_alg_values_supported")
val tokenEndpointAuthSigningAlgValuesSupported: List<String>,
@JsonProperty("userinfo_encryption_alg_values_supported")
val userinfoEncryptionAlgValuesSupported: List<String>,
@JsonProperty("userinfo_encryption_enc_values_supported")
val userinfoEncryptionEncValuesSupported: List<String>,
@JsonProperty("userinfo_endpoint")
val userinfoEndpoint: String,
@JsonProperty("userinfo_signing_alg_values_supported")
val userinfoSigningAlgValuesSupported: List<String>
) {
data class MtlsEndpointAliases(
@JsonProperty("backchannel_authentication_endpoint")
val backchannelAuthenticationEndpoint: String,
@JsonProperty("device_authorization_endpoint")
val deviceAuthorizationEndpoint: String,
@JsonProperty("introspection_endpoint")
val introspectionEndpoint: String,
@JsonProperty("pushed_authorization_request_endpoint")
val pushedAuthorizationRequestEndpoint: String,
@JsonProperty("registration_endpoint")
val registrationEndpoint: String,
@JsonProperty("revocation_endpoint")
val revocationEndpoint: String,
@JsonProperty("token_endpoint")
val tokenEndpoint: String,
@JsonProperty("userinfo_endpoint")
val userinfoEndpoint: String
)
}

View File

@ -0,0 +1,23 @@
import com.fasterxml.jackson.annotation.JsonProperty
import java.time.LocalDateTime
data class AuthTokenResponse(
@JsonProperty("access_token")
val accessToken: String,
@JsonProperty("expires_in")
val expiresIn: Int,
@JsonProperty("not-before-policy")
val notBeforePolicy: Int,
@JsonProperty("refresh_expires_in")
val refreshExpiresIn: Int,
@JsonProperty("refresh_token")
val refreshToken: String,
@JsonProperty("scope")
val scope: String,
@JsonProperty("session_state")
val sessionState: String,
@JsonProperty("token_type")
val tokenType: String,
val createdAt: LocalDateTime = LocalDateTime.now()
)

View File

@ -6,27 +6,26 @@ import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.restapi.config.AppConfig.Companion.appConfig import com.restapi.config.AppConfig.Companion.appConfig
import com.restapi.config.AuthUser
import io.ebean.Database import io.ebean.Database
import io.ebean.DatabaseFactory import io.ebean.DatabaseFactory
import io.ebean.config.CurrentTenantProvider import io.ebean.config.CurrentTenantProvider
import io.ebean.config.CurrentUserProvider import io.ebean.config.CurrentUserProvider
import io.ebean.config.DatabaseConfig import io.ebean.config.DatabaseConfig
import io.ebean.config.TenantMode import io.ebean.config.TenantMode
import redis.clients.jedis.JedisPooled
import java.util.* import java.util.*
import kotlin.jvm.optionals.getOrDefault
data class CurrentUser(
val anon: Boolean = true,
val userId: String = "",
val tenantId: String = ""
)
object Session { object Session {
private val currentUser = object : ThreadLocal<CurrentUser>() { private val currentUser = object : ThreadLocal<AuthUser>() {
override fun initialValue(): CurrentUser { override fun initialValue(): AuthUser {
return CurrentUser() return AuthUser("", "", emptyList<String>())
} }
} }
fun setAuthorizedUser(a: AuthUser) = currentUser.set(a)
private val sc = DatabaseConfig().apply { private val sc = DatabaseConfig().apply {
loadFromProperties(Properties().apply { loadFromProperties(Properties().apply {
setProperty("datasource.db.username", appConfig.dbUser()) setProperty("datasource.db.username", appConfig.dbUser())
@ -35,8 +34,8 @@ object Session {
setProperty("ebean.migration.run", appConfig.dbRunMigration().toString()) setProperty("ebean.migration.run", appConfig.dbRunMigration().toString())
}) })
tenantMode = TenantMode.PARTITION tenantMode = TenantMode.PARTITION
currentTenantProvider = CurrentTenantProvider { currentUser.get().tenantId } currentTenantProvider = CurrentTenantProvider { currentUser.get().tenant }
currentUserProvider = CurrentUserProvider { currentUser.get().userId } currentUserProvider = CurrentUserProvider { currentUser.get().userName }
} }
val database: Database = DatabaseFactory.create(sc) val database: Database = DatabaseFactory.create(sc)
@ -46,7 +45,7 @@ object Session {
findAndRegisterModules() findAndRegisterModules()
} }
fun currentUser() = currentUser.get().userId fun currentUser() = currentUser.get().userName
fun Database.findByEntityAndId(entity: String, id: String): DataModel { fun Database.findByEntityAndId(entity: String, id: String): DataModel {
return find(DataModel::class.java) return find(DataModel::class.java)
@ -68,6 +67,8 @@ object Session {
return String.format("%s-%s", entity, "$s".padStart(10, '0')) return String.format("%s-%s", entity, "$s".padStart(10, '0'))
} }
val redis = JedisPooled(appConfig.redisUri().getOrDefault("redis://localhost:6739/0"))
} }
object DataNotFoundException : Exception() { object DataNotFoundException : Exception() {