diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661e..31cca491 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/hawkscripts/authentication/form-auth-multi.kts b/hawkscripts/authentication/form-auth-multi.kts index 93e130de..230078fc 100644 --- a/hawkscripts/authentication/form-auth-multi.kts +++ b/hawkscripts/authentication/form-auth-multi.kts @@ -1,5 +1,4 @@ import com.fasterxml.jackson.databind.ObjectMapper -import com.stackhawk.Script import com.stackhawk.zap.extension.talon.HawkConfExtensions import com.stackhawk.zap.extension.talon.cleanHost import com.stackhawk.zap.extension.talon.hawkscan.ExtensionTalonHawkscan diff --git a/hawkscripts/authentication/okta-client-credentials-basic.kts b/hawkscripts/authentication/okta-client-credentials-basic.kts new file mode 100644 index 00000000..ec7a0109 --- /dev/null +++ b/hawkscripts/authentication/okta-client-credentials-basic.kts @@ -0,0 +1,85 @@ +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import java.util.Base64 +import java.util.TreeSet +import org.apache.commons.httpclient.URI +import org.apache.hc.client5.http.auth.AuthenticationException +import org.apache.hc.core5.http.ContentType +import org.apache.hc.core5.http.HttpHeaders +import org.apache.hc.core5.http.Method +import org.apache.log4j.LogManager +import org.parosproxy.paros.network.HtmlParameter +import org.parosproxy.paros.network.HttpHeader +import org.parosproxy.paros.network.HttpMessage +import org.parosproxy.paros.network.HttpRequestHeader +import org.zaproxy.zap.authentication.AuthenticationHelper +import org.zaproxy.zap.authentication.GenericAuthenticationCredentials + +val logger = LogManager.getLogger("okta-auth") +val mapper = ObjectMapper() + +fun authenticate( + helper: AuthenticationHelper, + paramsValues: Map, + credentials: GenericAuthenticationCredentials, +): HttpMessage { + logger.info("auth hook") + + val oktaDomain = paramsValues["okta_domain"] + val scope = paramsValues["scope"] + val clientId = credentials.getParam("client_id") + val clientSecret = credentials.getParam("client_secret") + val base64Creds = Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray()) + + + val msg = helper.prepareMessage() + msg.requestHeader = HttpRequestHeader( + Method.POST.name, + URI("https://$oktaDomain/oauth2/default/v1/token", true), + HttpHeader.HTTP11 + ) + msg.requestHeader.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()) + msg.requestHeader.addHeader(HttpHeaders.AUTHORIZATION, "Basic $base64Creds") + + val formTree = TreeSet() + formTree.add(HtmlParameter(HtmlParameter.Type.form, "grant_type", "client_credentials")) + formTree.add(HtmlParameter(HtmlParameter.Type.form, "scope", scope)) + msg.requestBody.setFormParams(formTree) + msg.requestHeader.contentLength = msg.requestBody.length() + + logger.info("::::::auth request:::::\n${msg.requestHeader}${msg.requestBody}") + + helper.sendAndReceive(msg) + + logger.info("::::::auth response:::::\n${msg.responseHeader}${msg.responseBody}") + + // Throw an authentication exception if the status code is not 2xx + if (!(200..299).contains(msg.responseHeader.statusCode)) { + val jsonObject = mapper.readValue(msg.responseBody.bytes, ObjectNode::class.java) + val err = jsonObject.get("error").asText() + val errDesc = jsonObject.get("error_description").asText() + throw AuthenticationException("$err $errDesc") + } + + return msg +} + +fun getRequiredParamsNames(): Array { + return arrayOf("okta_domain", "scope") +} + +fun getCredentialsParamsNames(): Array { + return arrayOf("client_id", "client_secret") +} + +fun getOptionalParamsNames(): Array { + return arrayOf() +} + +fun getLoggedInIndicator(): String { + return "" +} + +fun getLoggedOutIndicator(): String { + return "" +} diff --git a/hawkscripts/hawkscripts.gradle.kts b/hawkscripts/hawkscripts.gradle.kts index faccb1ac..e733e0b8 100644 --- a/hawkscripts/hawkscripts.gradle.kts +++ b/hawkscripts/hawkscripts.gradle.kts @@ -1,11 +1,11 @@ import org.jetbrains.kotlin.konan.file.File.Companion.userHome plugins { - kotlin("jvm") version "1.7.20" + kotlin("jvm") version "1.8.22" } val kotlinVersion = "1.7.20" -val hawkScriptSdkVersion = "3.1.12" +val hawkScriptSdkVersion = "3.4.2" kotlin { sourceSets { diff --git a/hawkscripts/session/jwt-session.kts b/hawkscripts/session/jwt-session.kts new file mode 100644 index 00000000..a96329ed --- /dev/null +++ b/hawkscripts/session/jwt-session.kts @@ -0,0 +1,62 @@ +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT +import java.time.Instant +import org.apache.log4j.LogManager +import org.zaproxy.zap.session.ScriptBasedSessionManagementMethodType +import org.zaproxy.zap.extension.script.ScriptVars + +val logger = LogManager.getLogger("okta-json-to-token") +val mapper = ObjectMapper() + +fun extractWebSession(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) { + + val tokenField = sessionWrapper.getParam("jwt_token_field") + val tokenType = sessionWrapper.getParam("token_type_field") ?: "Bearer" + + logger.info("get token from json: ${sessionWrapper.httpMessage.responseBody}") + val jsonObject = mapper.readValue(sessionWrapper.httpMessage.responseBody.bytes, ObjectNode::class.java) + val accessToken = jsonObject.get(tokenField).asText() + ScriptVars.setGlobalVar("auth_header_value", "$tokenType $accessToken") + + sessionWrapper.session.setValue("jwt", accessToken) + + val jwt = SignedJWT.parse(accessToken) + logger.info("jwt-expires: ${jwt.jwtClaimsSet.expirationTime}") + sessionWrapper.session.setValue("jwt_claims", jwt.jwtClaimsSet) +} + +fun processMessageToMatchSession(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) { + + val nowish = Instant.now().minusMillis(15000) + val jwtClaims = sessionWrapper.session.getValue("jwt_claims") as JWTClaimsSet? + val isExpired = jwtClaims?.expirationTime?.toInstant()?.isBefore(nowish) + + if (isExpired == true) { + logger.info("session expires @ ${jwtClaims.expirationTime}") + synchronized(this) { + sessionWrapper.httpMessage.requestingUser.authenticate() + } + } + + logger.debug("session-jwt: ${sessionWrapper.session.getValue("jwt")}") + + val hdrVal = ScriptVars.getGlobalVar("auth_header_value") + logger.debug("auth_header_value: $hdrVal") + if (!hdrVal.isNullOrEmpty()) { + sessionWrapper.httpMessage.requestHeader.setHeader("Authorization", hdrVal) + } + +} + +fun clearWebSessionIdentifiers(sessionWrapper: ScriptBasedSessionManagementMethodType.SessionWrapper) { +} + +fun getRequiredParamsNames(): Array { + return arrayOf("jwt_token_field") +} + +fun getOptionalParamsNames(): Array { + return arrayOf("token_type_field") +} \ No newline at end of file diff --git a/src/main/java/hawk/MultiHttpSecurityConfig.java b/src/main/java/hawk/MultiHttpSecurityConfig.java index 77b5d92e..f3cca7a9 100644 --- a/src/main/java/hawk/MultiHttpSecurityConfig.java +++ b/src/main/java/hawk/MultiHttpSecurityConfig.java @@ -119,7 +119,7 @@ protected void configure(HttpSecurity http) throws Exception { } @Configuration - @Order(4) + @Order(5) public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { @@ -163,4 +163,23 @@ public UserDetailsService userDetailsService() { return new InMemoryUserDetailsManager(user); } + + // "/api/okta/**" + + @Configuration + @Order(4) + public static class OktaWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.antMatcher("/api/okta/**") + .httpBasic().disable() + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/api/okta/**") + .permitAll(); + ; + } + } } diff --git a/src/main/java/hawk/api/jwt/JwtUserController.java b/src/main/java/hawk/api/jwt/JwtUserController.java index e11673e5..8fcb310a 100644 --- a/src/main/java/hawk/api/jwt/JwtUserController.java +++ b/src/main/java/hawk/api/jwt/JwtUserController.java @@ -28,7 +28,7 @@ public JwtUserController(UserService userService, UserSearchService userSearchSe } @GetMapping("/search/") - public ResponseEntity searchAll() { + public ResponseEntity> searchAll() { Search search = new Search(""); return ResponseEntity.ok(this.userService.findUsersByName("")); } diff --git a/src/main/java/hawk/api/okta/OktaController.java b/src/main/java/hawk/api/okta/OktaController.java new file mode 100644 index 00000000..cddc161e --- /dev/null +++ b/src/main/java/hawk/api/okta/OktaController.java @@ -0,0 +1,33 @@ +package hawk.api.okta; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; + +@RestController +@RequestMapping("/api/okta") +public class OktaController { + + @GetMapping("/me/token") + public ResponseEntity me(HttpServletRequest request) { + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String name = headerNames.nextElement(); + String value = request.getHeader(name); + System.out.println(name + ": " + value); + } + String authToken = request.getHeader("Authorization"); + if (authToken == null || !authToken.startsWith("Bearer ")) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + OktaIdInfo oktaInfo = new OktaIdInfo(authToken); + return ResponseEntity.ok(oktaInfo); + } + +} + diff --git a/src/main/java/hawk/api/okta/OktaIdInfo.java b/src/main/java/hawk/api/okta/OktaIdInfo.java new file mode 100644 index 00000000..0f50d1a0 --- /dev/null +++ b/src/main/java/hawk/api/okta/OktaIdInfo.java @@ -0,0 +1,13 @@ +package hawk.api.okta; + +public class OktaIdInfo { + private final String token; + + public OktaIdInfo(String token) { + this.token = token; + } + + public String getToken() { + return token; + } +} diff --git a/src/main/java/hawk/controller/PayloadController.java b/src/main/java/hawk/controller/PayloadController.java index f29fd3d9..5fdeec5f 100644 --- a/src/main/java/hawk/controller/PayloadController.java +++ b/src/main/java/hawk/controller/PayloadController.java @@ -21,7 +21,7 @@ public class PayloadController { private static final char[] chars = new char[]{'a','b','c',' ','\n'}; private static Map payloadCache = new ConcurrentHashMap(); - @Value("${payload.startSize:2048}") + @Value("${payload.start-size:2048}") private int startPayloadSize = 2048; @Value("${payload.count:10}") diff --git a/src/main/resources/application-postgresql.yaml b/src/main/resources/application-postgresql.yaml index 6122fb46..d69b6b74 100644 --- a/src/main/resources/application-postgresql.yaml +++ b/src/main/resources/application-postgresql.yaml @@ -32,10 +32,6 @@ springdoc: api-docs: path: /openapi -payload: - start-size: 3096 - count: 20 - logging: level: org.hibernate.SQL: debug diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 1feb312c..c1e0e509 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -30,4 +30,4 @@ springdoc: payload: start-size: 3096 - count: 20 \ No newline at end of file + count: 10 \ No newline at end of file diff --git a/stackhawk.d/stackhawk-okta.yml b/stackhawk.d/stackhawk-okta.yml new file mode 100644 index 00000000..9fe349f1 --- /dev/null +++ b/stackhawk.d/stackhawk-okta.yml @@ -0,0 +1,37 @@ +app: + applicationId: ${APP_ID:dacc7d3e-babc-47d2-b040-ab117ab04526} + env: ${APP_ENV:dev} + host: ${APP_HOST:https://localhost:9000} + + authentication: + script: + name: okta-client-credentials-basic.kts + parameters: + okta_domain: '${OKTA_DOMAIN:changeme.okta.com}' + scope: api_key + credentials: + client_id: '${OKTA_CLIENT_ID:changeme}' + client_secret: '${OKTA_CLIENT_SECRET:changeme}' + sessionScript: + name: okta-json-to-token.kts + parameters: + jwt_token_field: access_token + testPath: + path: /api/okta/me/token + success: 'HTTP(.*)200.*' + loggedInIndicator: '.*' + loggedOutIndicator: '' +hawkAddOn: + scripts: + - name: okta-client-credentials-basic.kts + type: authentication + path: hawkscripts + language: KOTLIN + - name: okta-json-to-token.kts + type: session + path: hawkscripts + language: KOTLIN + - name: log-http-payloads.kts + type: httpsender + path: hawkscripts + language: KOTLIN \ No newline at end of file diff --git a/stackhawk.d/stackhawk-openapi.yml b/stackhawk.d/stackhawk-openapi.yml index 05b46e10..4df5392a 100644 --- a/stackhawk.d/stackhawk-openapi.yml +++ b/stackhawk.d/stackhawk-openapi.yml @@ -25,6 +25,7 @@ app: testPath: path: /api/jwt/items/search/i success: "HTTP.*200.*" + openApiConf: # path: /openapi filePath: openapi.yaml diff --git a/stackhawk.yml b/stackhawk.yml index 61c291d0..5ae9e330 100644 --- a/stackhawk.yml +++ b/stackhawk.yml @@ -1,11 +1,11 @@ app: applicationId: ${APP_ID:dacc7d3e-babc-47d2-b040-ab117ab04526} - env: ${APP_ENV:dev} + env: ${APP_ENV:Development} host: ${APP_HOST:https://localhost:9000} excludePaths: - "/logout" - "/login-form-multi" -# - "/login-code" + - "/login-code" antiCsrfParam: "_csrf" authentication: loggedInIndicator: "\\QSign Out\\E" @@ -25,19 +25,5 @@ app: path: /search success: "HTTP.*200.*" waitForAppTarget: - pollDelay: 100 - waitTimeoutMillis: 1000 - -hawk: - scan: - concurrentRequests: 100 - throttlePassiveBacklog: 10000000 - spider: - maxDurationMinutes: 500 - config: - - "database.request.bodysize=30000000" - - "database.response.bodysize=30000000" - - "spider.maxParseSizeBytes=30000000" -# - "scanner.analyser.redirectEqualsNotFound=false" -# - "scanner.analyser.followRedirect=true" - + pollDelay: 500 + waitTimeoutMillis: 5000