Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5d018cc
captcha: Add captcha module
ebrahimmfadae Mar 7, 2022
59a5055
captcha: Add captcha service to docker-compose
ebrahimmfadae Mar 7, 2022
786c52b
captcha: Add captcha main controller
ebrahimmfadae Mar 7, 2022
e4f31c7
referral: Fix broken referral
ebrahimmfadae Mar 7, 2022
1c2d2c6
swagger: Cleanup configs
ebrahimmfadae Mar 7, 2022
62cb457
captcha: Add captcha to root
ebrahimmfadae Mar 7, 2022
4eefb71
captcha: Update verify captcha schema
ebrahimmfadae Mar 8, 2022
d1ff60c
captcha: Implement captcha generation service
ebrahimmfadae Mar 8, 2022
3a59e13
captcha: Merge create session with get captcha image
ebrahimmfadae Mar 9, 2022
10c63ef
captcha: Update api doc
ebrahimmfadae Mar 9, 2022
648b163
captcha: Use LRU cache as storage
ebrahimmfadae Mar 9, 2022
5e91cba
captcha: Add expiration window for captcha records
ebrahimmfadae Mar 9, 2022
82dc9cd
captcha: Change captcha verify controller
ebrahimmfadae Mar 9, 2022
4ef5805
captcha: Handle repetitive id generation
ebrahimmfadae Mar 10, 2022
cc6392e
captcha: Add custom session store
ebrahimmfadae Mar 10, 2022
bf9d851
captcha: Refactor captcha service api
ebrahimmfadae Mar 12, 2022
f798cff
captcha: Add captcha to keycloak registration flow
ebrahimmfadae Mar 12, 2022
326301c
captcha: Add captcha to forgot password
ebrahimmfadae Mar 12, 2022
3317724
Rename .java to .kt
ebrahimmfadae Mar 12, 2022
6e28389
captcha: Convert RegistrationOpexCaptcha to kotlin
ebrahimmfadae Mar 12, 2022
8eab8d9
Rename .java to .kt
ebrahimmfadae Mar 12, 2022
03b00de
captcha: Convert Captcha class to kotlin
ebrahimmfadae Mar 12, 2022
ae62e18
captcha: Add remote address to captcha process
ebrahimmfadae Mar 13, 2022
cf3e074
captcha: Rename some classes
ebrahimmfadae Mar 13, 2022
77bda34
captcha: Rename captcha req/res headers
ebrahimmfadae Mar 14, 2022
3099067
captcha: Fix captcha expire header
ebrahimmfadae Mar 14, 2022
279a306
Remove captcha once it's verified
ebrahimmfadae Apr 4, 2022
0f9683b
Merge branch 'dev' into 221-implement-captcha-service
ebrahimmfadae Apr 4, 2022
191936e
Fix auth module exceptions
ebrahimmfadae Apr 4, 2022
a0b1ba4
captcha: Fix captcha url in auth module
ebrahimmfadae Apr 4, 2022
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: 4 additions & 0 deletions captcha/captcha-app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FROM openjdk:11
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar /app.jar"]
89 changes: 89 additions & 0 deletions captcha/captcha-app/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<artifactId>captcha</artifactId>
<groupId>co.nilin.opex.captcha</groupId>
<version>1.0-SNAPSHOT</version>
</parent>

<groupId>co.nilin.opex.captcha.app</groupId>
<artifactId>captcha-app</artifactId>

<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor.kotlin</groupId>
<artifactId>reactor-kotlin-extensions</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>co.nilin.opex.utility.log</groupId>
<artifactId>logging-handler</artifactId>
</dependency>
<dependency>
<groupId>co.nilin.opex.utility.error</groupId>
<artifactId>error-handler</artifactId>
</dependency>
<dependency>
<groupId>co.nilin.opex.utility.interceptors</groupId>
<artifactId>interceptors</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package co.nilin.opex.captcha.app

import co.nilin.opex.utility.error.EnableOpexErrorHandler
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.ComponentScan

@SpringBootApplication
@ComponentScan("co.nilin.opex")
@EnableOpexErrorHandler
class App

fun main(args: Array<String>) {
runApplication<App>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package co.nilin.opex.captcha.app.api

interface CaptchaGenerator {
fun generate(): Pair<String, ByteArray>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package co.nilin.opex.captcha.app.api

interface SessionStore {
fun put(proof: String): Long
fun remove(proof: String): Boolean
fun verify(proof: String): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package co.nilin.opex.captcha.app.config

import org.springframework.context.annotation.Configuration

@Configuration
class AppConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package co.nilin.opex.captcha.app.config

import co.nilin.opex.utility.interceptors.FormDataWorkaroundFilter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.format.Formatter
import org.springframework.web.server.WebFilter
import java.util.*

@Configuration
class RestConfig {
@Bean
fun dateFormatter(): Formatter<Date?>? {
return object : Formatter<Date?> {
override fun print(date: Date, locale: Locale): String {
return date.time.toString()
}

override fun parse(date: String, locale: Locale): Date {
return Date(date.toLong())
}
}
}

@Bean
fun formDataWebFilter(): WebFilter {
return FormDataWorkaroundFilter()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package co.nilin.opex.captcha.app.config

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import springfox.documentation.builders.ApiInfoBuilder
import springfox.documentation.builders.PathSelectors
import springfox.documentation.service.ApiInfo
import springfox.documentation.spi.DocumentationType
import springfox.documentation.spring.web.plugins.Docket

@Configuration
class SwaggerConfig {
@Bean
fun opexCaptcha(): Docket {
return Docket(DocumentationType.SWAGGER_2)
.groupName("opex-captcha")
.apiInfo(apiInfo())
.select()
.paths(PathSelectors.regex("^/actuator.*").negate())
.build()
.useDefaultResponseMessages(false)
}

private fun apiInfo(): ApiInfo {
return ApiInfoBuilder()
.title("OPEX API")
.description("Backend for opex exchange.")
.license("MIT License")
.licenseUrl("https://github.com/opexdev/Back-end/blob/feature/1-MVP/LICENSE")
.version("0.1")
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package co.nilin.opex.captcha.app.controller

import co.nilin.opex.captcha.app.api.CaptchaGenerator
import co.nilin.opex.captcha.app.api.SessionStore
import co.nilin.opex.captcha.app.extension.sha256
import co.nilin.opex.utility.error.data.OpexError
import co.nilin.opex.utility.error.data.OpexException
import io.swagger.annotations.ApiOperation
import io.swagger.annotations.ApiResponse
import io.swagger.annotations.ApiResponses
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.util.*

@RestController
class Controller(
private val captchaGenerator: CaptchaGenerator, private val sessionStore: SessionStore
) {
@ApiOperation(
value = "Get captcha image", notes = "Get captcha image associated with provided id."
)
@ApiResponses(ApiResponse(message = "OK", code = 200))
@PostMapping("/session", produces = [MediaType.IMAGE_JPEG_VALUE])
suspend fun getCaptchaImage(
@RequestHeader("X-Forwarded-For", defaultValue = "0.0.0.0") xForwardedFor: List<String>
): ResponseEntity<ByteArray> {
fun idGen(id: String = UUID.randomUUID().toString().sha256()): String =
if (sessionStore.verify(id)) idGen() else id
val (answer, image) = captchaGenerator.generate()
val id = idGen()
val proof = "$id-$answer-${xForwardedFor.first()}".sha256()
return ResponseEntity(image, HttpHeaders().apply {
set("Captcha-Session-Key", id)
set("Captcha-Expire-Timestamp", (sessionStore.put(proof) / 1000).toString())
}, HttpStatus.OK)
}

@ApiOperation(
value = "Verify captcha",
notes = "Verify captcha. proof is a string in form of \"{{captcha-session-key}}-{{answer}}-{{remote-ip}}\""
)
@ApiResponses(
ApiResponse(message = "OK", code = 204), ApiResponse(message = "INVALID", code = 400)
)
@ResponseStatus(HttpStatus.NO_CONTENT)
@GetMapping("/verify")
suspend fun verifyCaptcha(@RequestParam proof: String) {
proof.sha256().let {
if (sessionStore.verify(it)) sessionStore.remove(it) else throw OpexException(OpexError.BadRequest)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package co.nilin.opex.captcha.app.extension

import java.security.MessageDigest

fun String.sha256() = MessageDigest
.getInstance("SHA-256")
.digest(this.toByteArray())
.fold("") { str, it -> str + "%02x".format(it) }
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package co.nilin.opex.captcha.app.service

import co.nilin.opex.captcha.app.api.CaptchaGenerator
import org.springframework.stereotype.Service

@Service
class CaptchaGeneratorImpl : CaptchaGenerator {
override fun generate(): Pair<String, ByteArray> {
val text = SimpleCaptcha.generateText()
val image = SimpleCaptcha.generateImage(text)
return text to image
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package co.nilin.opex.captcha.app.service

import co.nilin.opex.captcha.app.api.SessionStore
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service

@Service
class SessionStoreImpl(
private val map: MutableMap<String, Long>,
@Value("\${app.captcha-window-seconds}") private val captchaWindowSeconds: Long
) : SessionStore {
override fun put(proof: String): Long {
return (System.currentTimeMillis() + captchaWindowSeconds * 1000).also { map[proof] = it }
}

override fun remove(proof: String): Boolean = map.remove(proof)?.let { true } ?: false

override fun verify(proof: String): Boolean {
cleanExpired()
return proof in map
}

private fun cleanExpired() {
val ms = System.currentTimeMillis()
map.filterValues { it <= ms }.forEach { map.remove(it.key) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package co.nilin.opex.captcha.app.service

import java.awt.Color
import java.awt.Font
import java.awt.RenderingHints
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.util.*
import javax.imageio.ImageIO

object SimpleCaptcha {
/**
* Generates a random alpha-numeric string of eight characters.
*
* @return random alpha-numeric string of eight characters.
*/
fun generateText(): String {
return StringTokenizer(UUID.randomUUID().toString(), "-").nextToken()
}

/**
* Generates a PNG image of text 180 pixels wide, 40 pixels high with white background.
*
* @param text expects string size eight (8) characters.
* @return byte array that is a PNG image generated with text displayed.
*/
fun generateImage(text: String): ByteArray {
val w = 180
val h = 40
val image = BufferedImage(w, h, BufferedImage.TYPE_INT_RGB)
val g = image.createGraphics()
g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON)
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON)
g.color = Color.white
g.fillRect(0, 0, w, h)
g.font = Font("Serif", Font.PLAIN, 26)
g.color = Color.blue
val start = 10
val bytes = text.toByteArray()
val random = Random()
for (i in bytes.indices) {
g.color = Color(random.nextInt(255), random.nextInt(255), random.nextInt(255))
g.drawString(String(byteArrayOf(bytes[i])), start + i * 20, (Math.random() * 20 + 20).toInt())
}
g.color = Color.white
for (i in 0..7) {
g.drawOval((Math.random() * 160).toInt(), (Math.random() * 10).toInt(), 30, 30)
}
g.dispose()
val bout = ByteArrayOutputStream()
kotlin.runCatching { ImageIO.write(image, "png", bout) }.onFailure { throw RuntimeException(it) }
return bout.toByteArray()
}
}
23 changes: 23 additions & 0 deletions captcha/captcha-app/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
server.port: 8080
logging:
level:
co.nilin: DEBUG
reactor.netty.http.client: DEBUG
spring:
application:
name: opex-captcha
main:
allow-bean-definition-overriding: false
cloud:
bootstrap:
enabled: true
consul:
host: ${CONSUL_HOST:localhost}
port: 8500
discovery:
#healthCheckPath: ${management.context-path}/health
instance-id: ${spring.application.name}:${server.port}
healthCheckInterval: 20s
prefer-ip-address: true
app:
captcha-window-seconds: 600
Loading