From 98a506c97578125f888b5eaafe8a22cfd6246e1c Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 15:42:47 +0100 Subject: [PATCH 001/209] Listing 2.1 - update POM with our required dependencies. --- natter-api/pom.xml | 92 ++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 65 deletions(-) diff --git a/natter-api/pom.xml b/natter-api/pom.xml index d1c2a83..c3188ef 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -1,75 +1,37 @@ - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - com.manning.apisecurityinaction + com.manning.api-security-in-action natter-api - 1.0-SNAPSHOT - - natter-api - - http://www.example.com - + 1.0.0-SNAPSHOT - UTF-8 - 1.7 - 1.7 + 11 + 11 + + com.manning.apisecurityinaction.Main + - - junit - junit - 4.11 - test + com.h2database + h2 + 1.4.197 + + + com.sparkjava + spark-core + 2.7.2 + + + org.json + json + 20180813 + + + org.slf4j + slf4j-simple + 1.7.21 - - - - - - - maven-clean-plugin - 3.1.0 - - - - maven-resources-plugin - 3.0.2 - - - maven-compiler-plugin - 3.8.0 - - - maven-surefire-plugin - 2.22.1 - - - maven-jar-plugin - 3.0.2 - - - maven-install-plugin - 2.5.2 - - - maven-deploy-plugin - 2.8.2 - - - - maven-site-plugin - 3.7.1 - - - maven-project-info-reports-plugin - 3.0.0 - - - - - + \ No newline at end of file From d606a49b09ea83d09ea070e975642df7441be57f Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 15:45:25 +0100 Subject: [PATCH 002/209] Listing 2.2 Add the database schema SQL file --- natter-api/src/main/resources/schema.sql | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 natter-api/src/main/resources/schema.sql diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql new file mode 100644 index 0000000..8417d1b --- /dev/null +++ b/natter-api/src/main/resources/schema.sql @@ -0,0 +1,16 @@ +CREATE TABLE spaces( + space_id INT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + owner VARCHAR(30) NOT NULL +); +CREATE SEQUENCE space_id_seq; +CREATE TABLE messages( + space_id INT NOT NULL REFERENCES spaces(space_id), + msg_id INT PRIMARY KEY, + author VARCHAR(30) NOT NULL, + msg_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + msg_text VARCHAR(1024) NOT NULL +); +CREATE SEQUENCE msg_id_seq; +CREATE INDEX msg_timestamp_idx ON messages(msg_time); +CREATE UNIQUE INDEX space_name_idx ON spaces(name); \ No newline at end of file From 2c87eb5f02eb2e2a8d3adfb896224aa8ee173798 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 15:45:58 +0100 Subject: [PATCH 003/209] Remove Maven-generated App/AppTest classes --- .../com/manning/apisecurityinaction/App.java | 13 ------------ .../manning/apisecurityinaction/AppTest.java | 20 ------------------- 2 files changed, 33 deletions(-) delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/App.java delete mode 100644 natter-api/src/test/java/com/manning/apisecurityinaction/AppTest.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/App.java b/natter-api/src/main/java/com/manning/apisecurityinaction/App.java deleted file mode 100644 index 3bcfcb8..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/App.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.manning.apisecurityinaction; - -/** - * Hello world! - * - */ -public class App -{ - public static void main( String[] args ) - { - System.out.println( "Hello World!" ); - } -} diff --git a/natter-api/src/test/java/com/manning/apisecurityinaction/AppTest.java b/natter-api/src/test/java/com/manning/apisecurityinaction/AppTest.java deleted file mode 100644 index eb258be..0000000 --- a/natter-api/src/test/java/com/manning/apisecurityinaction/AppTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.manning.apisecurityinaction; - -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - -/** - * Unit test for simple App. - */ -public class AppTest -{ - /** - * Rigorous Test :-) - */ - @Test - public void shouldAnswerWithTrue() - { - assertTrue( true ); - } -} From 4037d975d611c3a2b0b1fad98741a73a37d8416f Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 15:50:05 +0100 Subject: [PATCH 004/209] Listing 2.3 Setting up the connection pool --- .../com/manning/apisecurityinaction/Main.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/Main.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java new file mode 100644 index 0000000..b6b710e --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -0,0 +1,26 @@ +package com.manning.apisecurityinaction; + +import java.nio.file.*; +import java.sql.Connection; + +import org.h2.jdbcx.JdbcConnectionPool; + +public class Main { + + public static void main(String... args) throws Exception { + var datasource = JdbcConnectionPool.create( + "jdbc:h2:mem:natter", "natter", "password"); + createTables(datasource.getConnection()); + } + + private static void createTables(Connection connection) throws Exception { + try (var conn = connection; + var stmt = conn.createStatement()) { + conn.setAutoCommit(false); + Path path = Paths.get( + Main.class.getResource("/schema.sql").toURI()); + stmt.execute(Files.readString(path)); + conn.commit(); + } + } +} \ No newline at end of file From 564ebfb0ea663d6322350238a76201122356c06c Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 15:55:15 +0100 Subject: [PATCH 005/209] Listing 2.4 The SpaceController class --- .../controller/SpaceController.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java new file mode 100644 index 0000000..94f34fd --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -0,0 +1,51 @@ +package com.manning.apisecurityinaction.controller; + +import java.sql.*; +import javax.sql.DataSource; +import org.json.JSONObject; +import spark.*; + +public class SpaceController { + + private final DataSource datasource; + + public SpaceController(DataSource datasource) { + this.datasource = datasource; + } + + public JSONObject createSpace(Request request, Response response) + throws SQLException { + var json = new JSONObject(request.body()); + var spaceName = json.getString("name"); + var owner = json.getString("owner"); + + try (var conn = datasource.getConnection(); + var stmt = conn.createStatement()) { + conn.setAutoCommit(false); + + var spaceId = firstLong(stmt.executeQuery( + "SELECT NEXT VALUE FOR space_id_seq;")); + + // WARNING: this next line of code contains a // security vulnerability! + stmt.executeUpdate( + "INSERT INTO spaces(space_id, name, owner) " + + "VALUES(" + spaceId + ", '" + spaceName + + "', '" + owner + "');"); + + conn.commit(); + response.status(201); + response.header("Location", "/spaces/" + spaceId); + + return new JSONObject() + .put("name", spaceName) .put("uri", "/spaces/" + spaceId); + } + } + + private static long firstLong(ResultSet resultSet) + throws SQLException { + if (!resultSet.next()) { + throw new IllegalArgumentException("no results"); + } + return resultSet.getLong(1); + } +} \ No newline at end of file From aa3b0c162aa6ff986540be1c143c87df8f009919 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 15:57:47 +0100 Subject: [PATCH 006/209] Listing 2.5 Wiring up the Natter API endpoints --- .../com/manning/apisecurityinaction/Main.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index b6b710e..3eceb1c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -1,9 +1,14 @@ package com.manning.apisecurityinaction; +import static spark.Spark.*; + import java.nio.file.*; import java.sql.Connection; import org.h2.jdbcx.JdbcConnectionPool; +import org.json.JSONObject; + +import com.manning.apisecurityinaction.controller.SpaceController; public class Main { @@ -11,6 +16,19 @@ public static void main(String... args) throws Exception { var datasource = JdbcConnectionPool.create( "jdbc:h2:mem:natter", "natter", "password"); createTables(datasource.getConnection()); + + var spaceController = + new SpaceController(datasource); + + post("/spaces", spaceController::createSpace); + afterAfter((request, response) -> { + response.type("application/json"); + }); + + internalServerError(new JSONObject() + .put("error", "internal server error").toString()); + notFound(new JSONObject() + .put("error", "not found").toString()); } private static void createTables(Connection connection) throws Exception { From f274036587b4c53119d440225b722aa56f4781ed Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 16:27:26 +0100 Subject: [PATCH 007/209] Listing 2.6 Using prepared statements --- .../controller/SpaceController.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index 94f34fd..b3c714f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -26,13 +26,15 @@ public JSONObject createSpace(Request request, Response response) var spaceId = firstLong(stmt.executeQuery( "SELECT NEXT VALUE FOR space_id_seq;")); - // WARNING: this next line of code contains a // security vulnerability! - stmt.executeUpdate( + var insertStmt = conn.prepareStatement( "INSERT INTO spaces(space_id, name, owner) " + - "VALUES(" + spaceId + ", '" + spaceName + - "', '" + owner + "');"); - + "VALUES(?, ?, ?);"); + insertStmt.setLong(1, spaceId); + insertStmt.setString(2, spaceName); + insertStmt.setString(3, owner); + insertStmt.executeUpdate(); conn.commit(); + response.status(201); response.header("Location", "/spaces/" + spaceId); From de2fcc3071142c5769a0f5ec2f61c7c87c2a411e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 16:29:37 +0100 Subject: [PATCH 008/209] Listing 2.7 Create restricted database user --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 2 ++ natter-api/src/main/resources/schema.sql | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 3eceb1c..ff8dd6f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -16,6 +16,8 @@ public static void main(String... args) throws Exception { var datasource = JdbcConnectionPool.create( "jdbc:h2:mem:natter", "natter", "password"); createTables(datasource.getConnection()); + datasource = JdbcConnectionPool.create( + "jdbc:h2:mem:natter", "natter_api_user", "password"); var spaceController = new SpaceController(datasource); diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 8417d1b..3c7b9d1 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -13,4 +13,7 @@ CREATE TABLE messages( ); CREATE SEQUENCE msg_id_seq; CREATE INDEX msg_timestamp_idx ON messages(msg_time); -CREATE UNIQUE INDEX space_name_idx ON spaces(name); \ No newline at end of file +CREATE UNIQUE INDEX space_name_idx ON spaces(name); + +CREATE USER natter_api_user PASSWORD 'password'; +GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; \ No newline at end of file From a0fb8e7c422e348a6e3944a3ed05d2a983d37bc0 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 16:34:23 +0100 Subject: [PATCH 009/209] Listing 2.8 Using the Dalesbred library --- natter-api/pom.xml | 5 +++ .../com/manning/apisecurityinaction/Main.java | 5 ++- .../controller/SpaceController.java | 40 ++++++------------- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/natter-api/pom.xml b/natter-api/pom.xml index c3188ef..3e6b4af 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -33,5 +33,10 @@ slf4j-simple 1.7.21 + + org.dalesbred + dalesbred + 1.3.0 + \ No newline at end of file diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index ff8dd6f..2a96be9 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -5,6 +5,7 @@ import java.nio.file.*; import java.sql.Connection; +import org.dalesbred.Database; import org.h2.jdbcx.JdbcConnectionPool; import org.json.JSONObject; @@ -19,8 +20,8 @@ public static void main(String... args) throws Exception { datasource = JdbcConnectionPool.create( "jdbc:h2:mem:natter", "natter_api_user", "password"); - var spaceController = - new SpaceController(datasource); + var database = Database.forDataSource(datasource); + var spaceController = new SpaceController(database); post("/spaces", spaceController::createSpace); afterAfter((request, response) -> { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index b3c714f..69c7fdd 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -1,53 +1,37 @@ package com.manning.apisecurityinaction.controller; -import java.sql.*; -import javax.sql.DataSource; +import org.dalesbred.Database; import org.json.JSONObject; + import spark.*; public class SpaceController { - private final DataSource datasource; + private final Database database; - public SpaceController(DataSource datasource) { - this.datasource = datasource; + public SpaceController(Database database) { + this.database = database; } - public JSONObject createSpace(Request request, Response response) - throws SQLException { + public JSONObject createSpace(Request request, Response response) { var json = new JSONObject(request.body()); var spaceName = json.getString("name"); var owner = json.getString("owner"); - try (var conn = datasource.getConnection(); - var stmt = conn.createStatement()) { - conn.setAutoCommit(false); - - var spaceId = firstLong(stmt.executeQuery( - "SELECT NEXT VALUE FOR space_id_seq;")); + return database.withTransaction(tx -> { + var spaceId = database.findUniqueLong( + "SELECT NEXT VALUE FOR space_id_seq;"); - var insertStmt = conn.prepareStatement( + database.updateUnique( "INSERT INTO spaces(space_id, name, owner) " + - "VALUES(?, ?, ?);"); - insertStmt.setLong(1, spaceId); - insertStmt.setString(2, spaceName); - insertStmt.setString(3, owner); - insertStmt.executeUpdate(); - conn.commit(); + "VALUES(?, ?, ?);", spaceId, spaceName, owner); response.status(201); response.header("Location", "/spaces/" + spaceId); return new JSONObject() .put("name", spaceName) .put("uri", "/spaces/" + spaceId); - } - } - private static long firstLong(ResultSet resultSet) - throws SQLException { - if (!resultSet.next()) { - throw new IllegalArgumentException("no results"); - } - return resultSet.getLong(1); + }); } } \ No newline at end of file From 1691cfae50328dd1edffa9f232d3c229869e9a45 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 16:35:58 +0100 Subject: [PATCH 010/209] Listing 2.9 Validating inputs --- .../apisecurityinaction/controller/SpaceController.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index 69c7fdd..4f11211 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -16,7 +16,13 @@ public SpaceController(Database database) { public JSONObject createSpace(Request request, Response response) { var json = new JSONObject(request.body()); var spaceName = json.getString("name"); + if (spaceName.length() > 255) { + throw new IllegalArgumentException("space name too long"); + } var owner = json.getString("owner"); + if (!owner.matches("[a-zA-Z][a-zA-Z0-9]{1,29}")) { + throw new IllegalArgumentException("invalid owner name"); + } return database.withTransaction(tx -> { var spaceId = database.findUniqueLong( From ba7829984277ba630f01859a72391f1f0e072962 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 16:41:29 +0100 Subject: [PATCH 011/209] Listing 2.10 Handling exceptions --- .../java/com/manning/apisecurityinaction/Main.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 2a96be9..c1e26ef 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -7,10 +7,12 @@ import org.dalesbred.Database; import org.h2.jdbcx.JdbcConnectionPool; -import org.json.JSONObject; +import org.json.*; import com.manning.apisecurityinaction.controller.SpaceController; +import spark.*; + public class Main { public static void main(String... args) throws Exception { @@ -32,8 +34,17 @@ public static void main(String... args) throws Exception { .put("error", "internal server error").toString()); notFound(new JSONObject() .put("error", "not found").toString()); + + exception(IllegalArgumentException.class, Main::badRequest); + exception(JSONException.class, Main::badRequest); } + private static void badRequest(Exception ex, + Request request, Response response) { + response.status(400); + response.body("{\"error\": \"" + ex + "\"}"); + } + private static void createTables(Connection connection) throws Exception { try (var conn = connection; var stmt = conn.createStatement()) { From 0f32bf3b14328909996a8bc5b5036a993a4e6396 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 16:44:35 +0100 Subject: [PATCH 012/209] Remove information leakage --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index c1e26ef..4aeee0d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -28,6 +28,7 @@ public static void main(String... args) throws Exception { post("/spaces", spaceController::createSpace); afterAfter((request, response) -> { response.type("application/json"); + response.header("Server", ""); }); internalServerError(new JSONObject() @@ -42,7 +43,7 @@ public static void main(String... args) throws Exception { private static void badRequest(Exception ex, Request request, Response response) { response.status(400); - response.body("{\"error\": \"" + ex + "\"}"); + response.body(new JSONObject().put("error", ex.getMessage()).toString()); } private static void createTables(Connection connection) throws Exception { From 067b05a72fe8ed92b09d545912e8a33f8a909ab5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 16:54:13 +0100 Subject: [PATCH 013/209] Exploiting XSS against the Natter API --- .../java/com/manning/apisecurityinaction/Main.java | 9 +++++++-- .../controller/SpaceController.java | 2 +- natter-api/xss.html | 13 +++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 natter-api/xss.html diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 4aeee0d..1aec8cd 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -26,9 +26,14 @@ public static void main(String... args) throws Exception { var spaceController = new SpaceController(database); post("/spaces", spaceController::createSpace); - afterAfter((request, response) -> { + after((request, response) -> { response.type("application/json"); + }); + afterAfter((request, response) -> { response.header("Server", ""); + + // Temporarily disable browser XSS protections + response.header("X-XSS-Protection", "0"); }); internalServerError(new JSONObject() @@ -43,7 +48,7 @@ public static void main(String... args) throws Exception { private static void badRequest(Exception ex, Request request, Response response) { response.status(400); - response.body(new JSONObject().put("error", ex.getMessage()).toString()); + response.body("{\"error\":\"" + ex.getMessage() + "\"}"); } private static void createTables(Connection connection) throws Exception { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index 4f11211..0b36d13 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -21,7 +21,7 @@ public JSONObject createSpace(Request request, Response response) { } var owner = json.getString("owner"); if (!owner.matches("[a-zA-Z][a-zA-Z0-9]{1,29}")) { - throw new IllegalArgumentException("invalid owner name"); + throw new IllegalArgumentException("invalid username: " + owner); } return database.withTransaction(tx -> { diff --git a/natter-api/xss.html b/natter-api/xss.html new file mode 100644 index 0000000..f56c677 --- /dev/null +++ b/natter-api/xss.html @@ -0,0 +1,13 @@ + + + +
+ +
+ + + \ No newline at end of file From dd8a4b336c855e1e64df09b7d51c77e3bf2bf7c1 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 17:05:12 +0100 Subject: [PATCH 014/209] Listing 2.12 Preventing XSS attacks --- .../com/manning/apisecurityinaction/Main.java | 22 +++++++++++++------ .../controller/SpaceController.java | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 1aec8cd..ba762a4 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -25,15 +25,23 @@ public static void main(String... args) throws Exception { var database = Database.forDataSource(datasource); var spaceController = new SpaceController(database); + before(((request, response) -> { + if (request.requestMethod().equals("POST") && + !"application/json".equals(request.contentType())) { + halt(406, new JSONObject().put( + "error", "Only application/json supported" + ).toString()); + } + })); + post("/spaces", spaceController::createSpace); - after((request, response) -> { - response.type("application/json"); - }); + afterAfter((request, response) -> { + response.type("application/json"); + response.header("X-Content-Type-Options", "nosniff"); + response.header("X-XSS-Protection", "1; mode=block"); + response.header("Cache-Control", "private, max-age=0"); response.header("Server", ""); - - // Temporarily disable browser XSS protections - response.header("X-XSS-Protection", "0"); }); internalServerError(new JSONObject() @@ -48,7 +56,7 @@ public static void main(String... args) throws Exception { private static void badRequest(Exception ex, Request request, Response response) { response.status(400); - response.body("{\"error\":\"" + ex.getMessage() + "\"}"); + response.body(new JSONObject().put("error", ex.getMessage()).toString()); } private static void createTables(Connection connection) throws Exception { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index 0b36d13..bae622e 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -21,7 +21,7 @@ public JSONObject createSpace(Request request, Response response) { } var owner = json.getString("owner"); if (!owner.matches("[a-zA-Z][a-zA-Z0-9]{1,29}")) { - throw new IllegalArgumentException("invalid username: " + owner); + throw new IllegalArgumentException("invalid username"); } return database.withTransaction(tx -> { From 4d58ffe7130b1835fc9ec60f90c4ae5945919b9a Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 17:08:08 +0100 Subject: [PATCH 015/209] Listing 2.13 Posting a new message --- .../com/manning/apisecurityinaction/Main.java | 1 + .../controller/SpaceController.java | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index ba762a4..fadb511 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -35,6 +35,7 @@ public static void main(String... args) throws Exception { })); post("/spaces", spaceController::createSpace); + post("/spaces/:spaceId/messages", spaceController::postMessage); afterAfter((request, response) -> { response.type("application/json"); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index bae622e..429989d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -40,4 +40,33 @@ public JSONObject createSpace(Request request, Response response) { }); } + + public JSONObject postMessage(Request request, Response response) { + var spaceId = Long.parseLong(request.params(":spaceId")); + var json = new JSONObject(request.body()); + var user = json.getString("author"); + if (!user.matches("[a-zA-Z][a-zA-Z0-9]{0,29}")) { + throw new IllegalArgumentException("invalid username"); + } + var message = json.getString("message"); + if (message.length() > 1024) { + throw new IllegalArgumentException("message is too long"); + } + + return database.withTransaction(tx -> { + var msgId = database.findUniqueLong( + "SELECT NEXT VALUE FOR msg_id_seq;"); + database.updateUnique( + "INSERT INTO messages(space_id, msg_id, msg_time," + + "author, msg_text) " + + "VALUES(?, ?, current_timestamp, ?, ?)", + spaceId, msgId, user, message); + + response.status(201); + var uri = "/spaces/" + spaceId + "/messages/" + msgId; + response.header("Location", uri); + return new JSONObject().put("uri", uri); + }); + } + } \ No newline at end of file From 8323e7d7ee5a32d60c1be33e91a6e8dd72af3fcc Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 17:10:53 +0100 Subject: [PATCH 016/209] Listing 2.14 Reading a single message --- .../com/manning/apisecurityinaction/Main.java | 5 +++ .../controller/SpaceController.java | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index fadb511..37a89dc 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -6,6 +6,7 @@ import java.sql.Connection; import org.dalesbred.Database; +import org.dalesbred.result.EmptyResultException; import org.h2.jdbcx.JdbcConnectionPool; import org.json.*; @@ -36,6 +37,8 @@ public static void main(String... args) throws Exception { post("/spaces", spaceController::createSpace); post("/spaces/:spaceId/messages", spaceController::postMessage); + get("/spaces/:spaceId/messages/:msgId", + spaceController::readMessage); afterAfter((request, response) -> { response.type("application/json"); @@ -52,6 +55,8 @@ public static void main(String... args) throws Exception { exception(IllegalArgumentException.class, Main::badRequest); exception(JSONException.class, Main::badRequest); + exception(EmptyResultException.class, + (e, request, response) -> response.status(404)); } private static void badRequest(Exception ex, diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index 429989d..fce4397 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -1,5 +1,7 @@ package com.manning.apisecurityinaction.controller; +import java.time.Instant; + import org.dalesbred.Database; import org.json.JSONObject; @@ -69,4 +71,43 @@ public JSONObject postMessage(Request request, Response response) { }); } + public Message readMessage(Request request, Response response) { + var spaceId = Long.parseLong(request.params(":spaceId")); + var msgId = Long.parseLong(request.params(":msgId")); + + var message = database.findUnique(Message.class, + "SELECT space_id, msg_id, author, msg_time, msg_text " + + "FROM messages WHERE msg_id = ? AND space_id = ?", + msgId, spaceId); + + response.status(200); + return message; + } + + public static class Message { + private final long spaceId; + private final long msgId; + private final String author; + private final Instant time; + private final String message; + + public Message(long spaceId, long msgId, String author, + Instant time, String message) { + this.spaceId = spaceId; + this.msgId = msgId; + this.author = author; + this.time = time; + this.message = message; + } + @Override + public String toString() { + JSONObject msg = new JSONObject(); + msg.put("uri", + "/spaces/" + spaceId + "/messages/" + msgId); + msg.put("author", author); + msg.put("time", time.toString()); + msg.put("message", message); + return msg.toString(); + } + } } \ No newline at end of file From c624e3a7d50d19bb37b49c6cf2c0dcb37e7144f5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 17:13:36 +0100 Subject: [PATCH 017/209] Listing 2.15 Searching for recent messages --- .../com/manning/apisecurityinaction/Main.java | 1 + .../controller/SpaceController.java | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 37a89dc..2d17841 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -39,6 +39,7 @@ public static void main(String... args) throws Exception { post("/spaces/:spaceId/messages", spaceController::postMessage); get("/spaces/:spaceId/messages/:msgId", spaceController::readMessage); + get("/spaces/:spaceId/messages", spaceController::findMessages); afterAfter((request, response) -> { response.type("application/json"); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index fce4397..72d2f4a 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -1,9 +1,11 @@ package com.manning.apisecurityinaction.controller; import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.stream.Collectors; import org.dalesbred.Database; -import org.json.JSONObject; +import org.json.*; import spark.*; @@ -84,6 +86,24 @@ public Message readMessage(Request request, Response response) { return message; } + public JSONArray findMessages(Request request, Response response) { + var since = Instant.now().minus(1, ChronoUnit.DAYS); + if (request.queryParams("since") != null) { + since = Instant.parse(request.queryParams("since")); + } + var spaceId = Long.parseLong(request.params(":spaceId")); + + var messages = database.findAll(Long.class, + "SELECT msg_id FROM messages " + + "WHERE space_id = ? AND msg_time >= ?;", + spaceId, since); + + response.status(200); + return new JSONArray(messages.stream() + .map(msgId -> "/spaces/" + spaceId + "/messages/" + msgId) + .collect(Collectors.toList())); + } + public static class Message { private final long spaceId; private final long msgId; From a1ddb72ba91e849476102f49a3c1af7fc4bb77a3 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 15 Apr 2019 17:18:26 +0100 Subject: [PATCH 018/209] Listing 2.16 The Moderator API --- .../com/manning/apisecurityinaction/Main.java | 7 +++++- .../controller/ModeratorController.java | 25 +++++++++++++++++++ natter-api/src/main/resources/schema.sql | 3 ++- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/ModeratorController.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 2d17841..ec0a0d5 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -10,7 +10,7 @@ import org.h2.jdbcx.JdbcConnectionPool; import org.json.*; -import com.manning.apisecurityinaction.controller.SpaceController; +import com.manning.apisecurityinaction.controller.*; import spark.*; @@ -41,6 +41,11 @@ public static void main(String... args) throws Exception { spaceController::readMessage); get("/spaces/:spaceId/messages", spaceController::findMessages); + var moderatorController = + new ModeratorController(database); + delete("/spaces/:spaceId/messages/:msgId", + moderatorController::deletePost); + afterAfter((request, response) -> { response.type("application/json"); response.header("X-Content-Type-Options", "nosniff"); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ModeratorController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ModeratorController.java new file mode 100644 index 0000000..294c4a5 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ModeratorController.java @@ -0,0 +1,25 @@ +package com.manning.apisecurityinaction.controller; + +import org.dalesbred.Database; +import org.json.JSONObject; + +import spark.*; + +public class ModeratorController { + + private final Database database; + + public ModeratorController(Database database) { + this.database = database; + } + + public JSONObject deletePost(Request request, Response response) { + var spaceId = Long.parseLong(request.params(":spaceId")); + var msgId = Long.parseLong(request.params(":msgId")); + + database.updateUnique("DELETE FROM messages " + + "WHERE space_id = ? AND msg_id = ?", spaceId, msgId); + response.status(200); + return new JSONObject(); + } +} diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 3c7b9d1..0a9a67e 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -16,4 +16,5 @@ CREATE INDEX msg_timestamp_idx ON messages(msg_time); CREATE UNIQUE INDEX space_name_idx ON spaces(name); CREATE USER natter_api_user PASSWORD 'password'; -GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; \ No newline at end of file +GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; +GRANT DELETE ON messages TO natter_api_user; \ No newline at end of file From 8969df75ffc6b65040ae5006b01d7138f4a10d6a Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 14:15:33 +0100 Subject: [PATCH 019/209] Add Guava dependency --- natter-api/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/natter-api/pom.xml b/natter-api/pom.xml index 3e6b4af..ebb40b4 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -38,5 +38,10 @@ dalesbred 1.3.0 + + com.google.guava + guava + 27.0.1-jre + \ No newline at end of file From 14755bd14035d906a486964032266bcc876bfa35 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 14:17:25 +0100 Subject: [PATCH 020/209] Listing 3.1 Add rate-limiting with Guava --- .../main/java/com/manning/apisecurityinaction/Main.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index ec0a0d5..ca4ebd3 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -10,6 +10,7 @@ import org.h2.jdbcx.JdbcConnectionPool; import org.json.*; +import com.google.common.util.concurrent.RateLimiter; import com.manning.apisecurityinaction.controller.*; import spark.*; @@ -26,6 +27,14 @@ public static void main(String... args) throws Exception { var database = Database.forDataSource(datasource); var spaceController = new SpaceController(database); + var rateLimiter = RateLimiter.create(2.0d); + + before((request, response) -> { + if (!rateLimiter.tryAcquire()) { + halt(429); + } + }); + before(((request, response) -> { if (request.requestMethod().equals("POST") && !"application/json".equals(request.contentType())) { From 4dff119c83578ac1a3736b7562adf09c9cdfc116 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 14:19:57 +0100 Subject: [PATCH 021/209] Add scrypt dependency --- natter-api/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/natter-api/pom.xml b/natter-api/pom.xml index ebb40b4..cacff9f 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -43,5 +43,10 @@ guava 27.0.1-jre + + com.lambdaworks + scrypt + 1.4.0 + \ No newline at end of file From 2cec9e3aceb95b47b37f4ddd4664e243f292d5f1 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 14:20:36 +0100 Subject: [PATCH 022/209] Add users table --- natter-api/src/main/resources/schema.sql | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 0a9a67e..937db6b 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -1,3 +1,8 @@ +CREATE TABLE users( + user_id VARCHAR(30) PRIMARY KEY, + pw_hash VARCHAR(255) NOT NULL +); + CREATE TABLE spaces( space_id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, From fbd26da92108f74d72d12c5563ba7132069f704a Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 14:22:45 +0100 Subject: [PATCH 023/209] Listing 3.2 Registering a new user --- .../controller/UserController.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java new file mode 100644 index 0000000..2c8301e --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -0,0 +1,43 @@ +package com.manning.apisecurityinaction.controller; + +import org.dalesbred.Database; +import org.json.JSONObject; + +import com.lambdaworks.crypto.SCryptUtil; + +import spark.*; + +public class UserController { + private static final String USERNAME_PATTERN = + "[a-zA-Z][a-zA-Z0-9]{1,29}"; + + private final Database database; + + public UserController(Database database) { + this.database = database; + } + + public JSONObject registerUser(Request request, + Response response) throws Exception { + var json = new JSONObject(request.body()); + var username = json.getString("username"); + var password = json.getString("password"); + + if (!username.matches(USERNAME_PATTERN)) { + throw new IllegalArgumentException("invalid username"); + } + if (password.length() < 8) { + throw new IllegalArgumentException( + "password must be at least 8 characters"); + } + + var hash = SCryptUtil.scrypt(password, 32768, 8, 1); + database.updateUnique( + "INSERT INTO users(user_id, pw_hash)" + + " VALUES(?, ?)", username, hash); + + response.status(201); + response.header("Location", "/users/" + username); + return new JSONObject().put("username", username); + } +} From b4feb61335f5863b95cd9cb488df96fe611c313e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 14:24:12 +0100 Subject: [PATCH 024/209] Add the user registration endpoint --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index ca4ebd3..c049f8e 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -26,6 +26,7 @@ public static void main(String... args) throws Exception { var database = Database.forDataSource(datasource); var spaceController = new SpaceController(database); + var userController = new UserController(database); var rateLimiter = RateLimiter.create(2.0d); @@ -44,6 +45,8 @@ public static void main(String... args) throws Exception { } })); + post("/users", userController::registerUser); + post("/spaces", spaceController::createSpace); post("/spaces/:spaceId/messages", spaceController::postMessage); get("/spaces/:spaceId/messages/:msgId", From 6ffbc81190037b465a7583c6438136e7c5de0ea8 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 14:32:03 +0100 Subject: [PATCH 025/209] Grant database user permissions on user table --- natter-api/src/main/resources/schema.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 937db6b..153373a 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -22,4 +22,5 @@ CREATE UNIQUE INDEX space_name_idx ON spaces(name); CREATE USER natter_api_user PASSWORD 'password'; GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; -GRANT DELETE ON messages TO natter_api_user; \ No newline at end of file +GRANT DELETE ON messages TO natter_api_user; +GRANT SELECT, INSERT ON users TO natter_api_user; \ No newline at end of file From d7998b1be00207f9bc1789ed33ad17a4ddaad1b5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 14:56:11 +0100 Subject: [PATCH 026/209] Listing 3.3 Authenticating a request --- .../controller/UserController.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 2c8301e..0b0f4d0 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -1,5 +1,8 @@ package com.manning.apisecurityinaction.controller; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + import org.dalesbred.Database; import org.json.JSONObject; @@ -40,4 +43,34 @@ public JSONObject registerUser(Request request, response.header("Location", "/users/" + username); return new JSONObject().put("username", username); } + + public void authenticate(Request request, Response response) { + var authHeader = request.headers("Authorization"); + if (authHeader == null || !authHeader.startsWith("Basic ")) { + return; + } + + var offset = "Basic ".length(); + var credentials = new String(Base64.getDecoder().decode( + authHeader.substring(offset)), StandardCharsets.UTF_8); + + var components = credentials.split(":", 2); + if (components.length != 2) { + throw new IllegalArgumentException("invalid auth header"); + } + + var username = components[0]; + var password = components[1]; + + if (!username.matches(USERNAME_PATTERN)) { + throw new IllegalArgumentException("invalid username"); + } + + var hash = database.findOptional(String.class, + "SELECT pw_hash FROM users WHERE user_id = ?", username); + + if (hash.isPresent() && SCryptUtil.check(password, hash.get())) { + request.attribute("subject", username); + } + } } From 04e6ea07085ac18ee4afa8e4f4049212d99b6e66 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:00:47 +0100 Subject: [PATCH 027/209] Add authentication checks --- .../main/java/com/manning/apisecurityinaction/Main.java | 2 ++ .../apisecurityinaction/controller/SpaceController.java | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index c049f8e..db9346b 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -45,6 +45,8 @@ public static void main(String... args) throws Exception { } })); + before(userController::authenticate); + post("/users", userController::registerUser); post("/spaces", spaceController::createSpace); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index 72d2f4a..cfc2816 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -27,6 +27,11 @@ public JSONObject createSpace(Request request, Response response) { if (!owner.matches("[a-zA-Z][a-zA-Z0-9]{1,29}")) { throw new IllegalArgumentException("invalid username"); } + var subject = request.attribute("subject"); + if (!owner.equals(subject)) { + throw new IllegalArgumentException( + "owner must match authenticated user"); + } return database.withTransaction(tx -> { var spaceId = database.findUniqueLong( @@ -52,6 +57,10 @@ public JSONObject postMessage(Request request, Response response) { if (!user.matches("[a-zA-Z][a-zA-Z0-9]{0,29}")) { throw new IllegalArgumentException("invalid username"); } + if (!user.equals(request.attribute("subject"))) { + throw new IllegalArgumentException( + "author must match authenticated user"); + } var message = json.getString("message"); if (message.length() > 1024) { throw new IllegalArgumentException("message is too long"); From 1b6c554efb45dfa9810d742ee32d804e7e036e75 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:05:03 +0100 Subject: [PATCH 028/209] Listing 3.4 Enabling HTTPS --- natter-api/localhost.p12 | Bin 0 -> 3975 bytes natter-api/server.pem | 26 ++++++++++++++++++ .../com/manning/apisecurityinaction/Main.java | 1 + 3 files changed, 27 insertions(+) create mode 100644 natter-api/localhost.p12 create mode 100644 natter-api/server.pem diff --git a/natter-api/localhost.p12 b/natter-api/localhost.p12 new file mode 100644 index 0000000000000000000000000000000000000000..3a8322f5305cbb79f7aed716437ebc6451479ede GIT binary patch literal 3975 zcmV;24|wn}f)9fN0Ru3C4^IXODuzgg_YDCD0ic2pKm>vhJTQU}I52_Ye! z2_OL%0s;sCfPx9A3gMG1a@{4An*!lOgxF4^%uOz^AB=R{DujF+60C5cS5s@@mVZg8 zM4)qywVokPU8ME(bkuL=&(9>0Q7^&X(MKb+63l<>Om)|F^={ zSdqUXf9u{8Yf@=i5z1jFjKUyWvh>uP$x(&@;Br2D);tSfs!+mTbiWUUC*Xqet&AW= zzn+9ll#%IMLWR^qx);y1q0B-vLZG_G@k!1gO=^f;xVf0R7v6p!{ZvEF7JqPiVN z<|h%~aqTJ4m)Y#~%||$GG`HABqFrM%sJ1VQxYhu)KfY#KU9?)-`(H`n3tu<~j8!6$ zm7UM7xqH{r@78dt);Emb&SJxNSO*d1iD(XQEwF-E@+*)>`_yu=7(=9Z#Dgd%w%t|Y zK%~oAQcamcVoPs$tTp^*2Xm38kX^_kL`h5KbZpGs{l56`gJlaxo(~|-D6L&3J@T?oKVPn%%2#mx2r9l?^|(aW>k`Qc z=UOwD4{7O+ZsTk|8YUj(o02mkc5S6D1Xk)Fz$pYI1>m0waa1$Sz*@EH6_jG+5ckS_ zhAfmXMbgh`W#j|Y+tm7BF^m^jEw zmvx90Q>BTjyHUva`KX8Ednz+10izP1QT+yH%6M$!inj{4GW4z__7V&&>z+(n8i&+@ zlt>JTT)Ju2PzX*E;RgZ}3f0I6{`#G)d>Pgoo9jJCdY4O%WxW#rOP#5bB8foQ%}63q z;&UctdFV>2gmP>5tFEwTA~U%zWJKpjQ%Ak3KE^4jk{5Q2C7wMrVvw8ZqqxA}_Z@9I z%D(cPg5<49k$$a#{O&&n4+Uzz0^9V4-XSoGd>EGDIaN+M_|ct5Mp__{f{1!Lj2y$z)SVS>epx z^tVR>G+mPc%3!L+k;#9({7H{87k;k*iLoC$IDmvoUKF*x+cc17t{|{~H|sLT#qoDJthJP$)9-I31&>AE!Oj(75W(dyezuX%o z3vQKdd;?v!H9ylve>qkhcs7bb3}0wwCW0B@0GltelFsax0Z4%mNwjU7Uwa6xeq<3@ zYeeE;x}wOH0u6?c6~ajJ{E@Z5EOYjEP@8qL&M?q|Tft?yW1H|WK{rQS*3En>Aq?%iSVb9}>=d*3!2rxm*QH$nB1>59A z{7=rX_H=`dMk)iOKTqa&vte{*+Ci-}keVtm+9Wh)z2u3|iU0;u$#$01lTBv#4m7%$ zl}z|FcuO>Q!F%|Y844AczeqDih%1*)OBeoN+iy^~Q>F$D%aS;eq|j_JeX(L%-!b3I znxCpH(n0?EITc9V{8$nWlAlt`c`J``>CH_|BND_kx0!7=fg7m+cnC5Q!KW@OMwAA6 z(#(?|$JogSt~Z@6tW#5ByjF|*-Ix{-2jPHS(`{eMxrL`in7uSrJ@7Y z`r0L}C4f>!Cc(DEz?(lHy3gv(+E-w}M(!NC8)9b+5;mdUyJJ1oj)}*?=C|M-j=!xjpvq-osfcrNTI zByy}LdAJlgjpSC?7-&?EJLNo%Te8X_07*OVO)&<-H9*#Zd zt7j%T5*luW03tj|$qf8H(j;3kzQnNgL0})@)8s+y81~FnKhwW>JFkFZwi#`@+@{_#yPnR%BJbncjmOpubP!(L^Lr``f( zlobIe*7ney*ZsL|z!R8Ci#utrV%Dn~YhIx0L&89X!{n?rXih2#ISwL{oMTzmctV2{ z%YGM(xY8vKHqnjF2ITQFXyNRjdckR;gXEJqZtrqsyXW_irLdV0rV}jxde0`Vd%>$g z$}0R~N}0Ytv8`_4gRp8d>8WHhriLu^kn&9roB$(wcEFr4oUmy^FoFd^1_>&LNQU2TRo%9s>#^f@$|!XmE`r18(nSL)?Gw{(3h zs%{P8m}UR%?UWbOq^4fA8wofg(t9ScUTwbJcKAd=DO3yCjwVZgqd%a|OiQcui*yWY z(n-|WTeU@0;}3ddeSJ%2x6#T4WSah8<`m<4E^WNNYbFO^6cd=5lW!tzfRH+Y_wAbC zW~8XQqWcSg<+{PUV|PLe&Nvv5^SJj=8NOdy_MfLgtIs)7VIX}DJ+a_|0{(&uSTG!|AxXD;h;7~tKC#K`$G_1Fr z{xGFY`yEzy<%N6!`}C`o5p~(f3YlRd3b)`E1l`Nm)98?ST}ucH8^E9MHi;*0z{E4% zWXPPPtxA~C4*NdEi z)5@{t2^lv}F~h0-$-+M!H<$Qm(2H4a{3PIxid{Fyd8Y=Dan!UAM5sJbe(QA?Ccj<8 zAM^pr*AmO6n;cu2c8U8-+P^@PYqWJf?95i{NUUPHvXuA!x|H+cJEA*S|X*KDSCVzkAh z)TG=FLxN=J6$W{T>yJoQiO&0cVHbw7h!WaLT~top+7d43E}~VorOHOL`b%cQ#9m=Ya%X#> z2kPdAEjJC4p0mCd%njz%`Px@^D%9-lxt#DvKF;ap-tbpj@m4}QZn@iXHY)cg z%*%4>KGjY50|5ZC!D#cL!%&}cMQ2{FDy@GjP||(i$UF`l>&*_%E`7(0B*c%hn%mtM zl8#g&HqQIMOGTF4brhQ(Gr=F+-Aj}hhb$GXG*z|`Zh|GQRUXDHpZc8=9k9(@XP?46 z-BGT3UG8QJbG&S_UZIWy`P-9_AcPxWrz)o`3JH6XXx`-?mv`hEb-+a2RA~z5pcO#{ zPN-Im!;20;+B?_$V(*K4xvue4Q>T+ZSk$JueBxJ7YcNtDkJJ~?6;IM5LqYMh=pG6C z3~6$@Vpsk&zHkGIM`Vq^Yg107UnK+u?g!R7BY?JU2c_zw4#4R<#Xst4QWE*Uu4&=a z3rE(184;~+WGJ?6K9IA$oJ)RMOgHCX+#*zIRQ&o6uZXNMsFm$Xg7NVVY+hm literal 0 HcmV?d00001 diff --git a/natter-api/server.pem b/natter-api/server.pem new file mode 100644 index 0000000..57f1306 --- /dev/null +++ b/natter-api/server.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEVTCCAr2gAwIBAgIQbxKpXUlxdww7SpPlkp9sPzANBgkqhkiG9w0BAQsFADB5 +MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExJzAlBgNVBAsMHm5laWxA +Z3Vlc3Qycy1NYWNCb29rLVByby5sb2NhbDEuMCwGA1UEAwwlbWtjZXJ0IG5laWxA +Z3Vlc3Qycy1NYWNCb29rLVByby5sb2NhbDAeFw0xOTA1MTQxNDAxMjhaFw0yOTA1 +MTQxNDAxMjhaMGYxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZp +Y2F0ZTEnMCUGA1UECwwebmVpbEBndWVzdDJzLU1hY0Jvb2stUHJvLmxvY2FsMRIw +EAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQC1uG++EhRXI07PqEU3mUy1Nh44w+5LTV48UvPm1wQSweigf8H3OldBqanl6f6V +WcElGRgg5kJTI/Q6Vst83Aq7d5mSCEx6x1Qxdymk/4Qmk8kbxXNXcQbLbpK7ubzp +JTkAIVlp8FaSEumWzlBH5PYvCE5Md2G6A/j6HviYGdqd2WPQ9asRNaPzZQ1pNYuj ++efY3mXZrHCC130D4WLGEkjOvpip/NxkTDfT8bTUBJwktPhAfMWox9LPb+dcUwVy +ovHqzGtQ8+lk4wC2xlZKJOkgCAnl/C+rre0wCUuDM3ZJIGtUAJPldwhKCRreyVR5 +UwuYJBuVf82I5WvxxZS6GqNnAgMBAAGjbDBqMA4GA1UdDwEB/wQEAwIFoDATBgNV +HSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFNfG9Ux4 +W+6l222e9FCr/mUQ3uQuMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0B +AQsFAAOCAYEAo0vnIX0TAqVeDvLPm4F9nG/ZV6WwRQjn82napuhWTTn1wL/99Fm9 +YyweM2O5AbF4oBNqPGNuGcGEGrmBTlCZQmPGFp98dq1I+iAIRkAabUAeLYJ9SsT4 +GS8o0rzEKhVYYfovs9cNVGRSL9rmVvVP1npA/W2+wZ8yJOEzSY8mYMbzXd+/uU83 +sRifuofEuzZk/NY+h16T27PMTN+bgYsPZManNeMoKejH7R7a6ksqXmSApL/dPL7O +fI5DIjAam2+y8QzlqJGI+PzMsHZXVeWxI4acywjSINxsQrdVhukcgLfSMeNE/nRv +5SLWTDQksKzCcrIQg5Is55FH50W6mAeGbeDBKV9488PihIeDF3SKwHbVKICuK2sQ +5gIPVMbdFDYma8QyQwPdpDo7FKAhtDIL4ktaJMYBq1qPt6GbZhGweJlysXYtBTcf +r9l50PMjd3wKK7YxApnfYBfuadP5AVd7A/kHmvn+GlroTVTr0XzKcYI5Sk1HWIcv +C0HoMYDueVuj +-----END CERTIFICATE----- diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index db9346b..cff1e33 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -18,6 +18,7 @@ public class Main { public static void main(String... args) throws Exception { + secure("localhost.p12", "changeit", null, null); var datasource = JdbcConnectionPool.create( "jdbc:h2:mem:natter", "natter", "password"); createTables(datasource.getConnection()); From ea2fd92ab0543fed0061f9405970a2e8625cc934 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:07:21 +0100 Subject: [PATCH 029/209] Add audit log table --- natter-api/src/main/resources/schema.sql | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 153373a..7edc349 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -20,7 +20,18 @@ CREATE SEQUENCE msg_id_seq; CREATE INDEX msg_timestamp_idx ON messages(msg_time); CREATE UNIQUE INDEX space_name_idx ON spaces(name); +CREATE TABLE audit_log( + audit_id INT NULL, + method VARCHAR(10) NOT NULL, + path VARCHAR(100) NOT NULL, + user_id VARCHAR(30) NULL, + status INT NULL, + audit_time TIMESTAMP NOT NULL +); +CREATE SEQUENCE audit_id_seq; + CREATE USER natter_api_user PASSWORD 'password'; GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; GRANT DELETE ON messages TO natter_api_user; -GRANT SELECT, INSERT ON users TO natter_api_user; \ No newline at end of file +GRANT SELECT, INSERT ON users TO natter_api_user; +GRANT SELECT, INSERT ON audit_log TO natter_api_user; \ No newline at end of file From 1e55fbc77edac416ec71366d62397d23c2f52764 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:08:37 +0100 Subject: [PATCH 030/209] Listing 3.5 The audit log controller --- .../controller/AuditController.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuditController.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuditController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuditController.java new file mode 100644 index 0000000..baeee2f --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuditController.java @@ -0,0 +1,89 @@ +package com.manning.apisecurityinaction.controller; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.stream.Collectors; + +import org.dalesbred.Database; +import org.json.*; + +import spark.*; + +public class AuditController { + + private final Database database; + + public AuditController(Database database) { + this.database = database; + } + + public void auditRequestStart(Request request, Response response) { + database.withVoidTransaction(tx -> { + var auditId = database.findUniqueLong( + "SELECT NEXT VALUE FOR audit_id_seq"); + request.attribute("audit_id", auditId); + database.updateUnique( + "INSERT INTO audit_log(audit_id, method, path, " + + "user_id, audit_time) " + + "VALUES(?, ?, ?, ?, current_timestamp)", + auditId, + request.requestMethod(), + request.pathInfo(), + request.attribute("subject")); + }); + } + public void auditRequestEnd(Request request, Response response) { + database.updateUnique( + "INSERT INTO audit_log(audit_id, method, path, status, " + + "user_id, audit_time) " + + "VALUES(?, ?, ?, ?, ?, current_timestamp)", + request.attribute("audit_id"), + request.requestMethod(), + request.pathInfo(), + response.status(), + request.attribute("subject")); + } + + public JSONArray readAuditLog(Request request, Response response) { + var since = Instant.now().minus(1, ChronoUnit.HOURS); + + var logs = database.findAll(LogRecord.class, + "SELECT audit_id, method, path, status, user_id, " + + "audit_time FROM audit_log WHERE audit_time >= ?", + since); + + return new JSONArray(logs.stream() + .map(LogRecord::toJson) + .collect(Collectors.toList())); + } + + public static class LogRecord { + private final Long auditId; + private final String method; + private final String path; + private final Integer status; + private final String user; + private final Instant auditTime; + + + public LogRecord(Long auditId, String method, String path, + Integer status, String user, Instant auditTime) { + this.auditId = auditId; + this.method = method; + this.path = path; + this.status = status; + this.user = user; + this.auditTime = auditTime; + } + + JSONObject toJson() { + return new JSONObject() + .put("id", auditId) + .put("method", method) + .put("path", path) + .put("status", status) + .put("user", user) + .put("time", auditTime.toString()); + } + } +} From 490adcdc2733d817d4284908f6eed352faca3759 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:10:38 +0100 Subject: [PATCH 031/209] Wire up audit logging --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index cff1e33..df376c3 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -48,6 +48,12 @@ public static void main(String... args) throws Exception { before(userController::authenticate); + var auditController = new AuditController(database); + before(auditController::auditRequestStart); + afterAfter(auditController::auditRequestEnd); + + get("/logs", auditController::readAuditLog); + post("/users", userController::registerUser); post("/spaces", spaceController::createSpace); From 58e8d18b0fbd27ccb2873f0cd7911cf143c729cd Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:15:08 +0100 Subject: [PATCH 032/209] Require authentication --- .../java/com/manning/apisecurityinaction/Main.java | 1 + .../apisecurityinaction/controller/UserController.java | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index df376c3..3db63d2 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -56,6 +56,7 @@ public static void main(String... args) throws Exception { post("/users", userController::registerUser); + before("/spaces", userController::requireAuthentication); post("/spaces", spaceController::createSpace); post("/spaces/:spaceId/messages", spaceController::postMessage); get("/spaces/:spaceId/messages/:msgId", diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 0b0f4d0..8f71648 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -1,5 +1,7 @@ package com.manning.apisecurityinaction.controller; +import static spark.Spark.halt; + import java.nio.charset.StandardCharsets; import java.util.Base64; @@ -73,4 +75,12 @@ public void authenticate(Request request, Response response) { request.attribute("subject", username); } } + + public void requireAuthentication(Request request, Response response) { + if (request.attribute("subject") == null) { + response.header("WWW-Authenticate", + "Basic realm=\"/\", charset=\"UTF-8\""); + halt(401); + } + } } From 4b0d0eebd5d4dc9e498530e9a51acfc1caefc0cc Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:17:43 +0100 Subject: [PATCH 033/209] Add permissions table and ensure space owner has full perms --- .../apisecurityinaction/controller/SpaceController.java | 4 ++++ natter-api/src/main/resources/schema.sql | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index cfc2816..ef9627d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -41,6 +41,10 @@ public JSONObject createSpace(Request request, Response response) { "INSERT INTO spaces(space_id, name, owner) " + "VALUES(?, ?, ?);", spaceId, spaceName, owner); + database.updateUnique( + "INSERT INTO permissions(space_id, user_id, perms) " + + "VALUES(?, ?, ?)", spaceId, owner, "rwd"); + response.status(201); response.header("Location", "/spaces/" + spaceId); diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 7edc349..0aa165e 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -2,6 +2,12 @@ CREATE TABLE users( user_id VARCHAR(30) PRIMARY KEY, pw_hash VARCHAR(255) NOT NULL ); +CREATE TABLE permissions( + space_id INT NOT NULL REFERENCES spaces(space_id), + user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), + perms VARCHAR(3) NOT NULL, + PRIMARY KEY (space_id, user_id) +); CREATE TABLE spaces( space_id INT PRIMARY KEY, @@ -34,4 +40,5 @@ CREATE USER natter_api_user PASSWORD 'password'; GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; GRANT DELETE ON messages TO natter_api_user; GRANT SELECT, INSERT ON users TO natter_api_user; -GRANT SELECT, INSERT ON audit_log TO natter_api_user; \ No newline at end of file +GRANT SELECT, INSERT ON audit_log TO natter_api_user; +GRANT SELECT, INSERT ON permissions TO natter_api_user; \ No newline at end of file From 99b64c0efafa7b3440d87b785f37bebea4e1d7c7 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:20:42 +0100 Subject: [PATCH 034/209] Listing 3.6 Checking permissions in a filter --- .../controller/UserController.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 8f71648..2555b6c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -83,4 +83,26 @@ public void requireAuthentication(Request request, Response response) { halt(401); } } + + public Filter requirePermission(String method, String permission) { + return (request, response) -> { + if (!method.equals(request.requestMethod())) { + return; + } + + requireAuthentication(request, response); + + var spaceId = Long.parseLong(request.params(":spaceId")); + var username = (String) request.attribute("subject"); + + var perms = database.findOptional(String.class, + "SELECT perms FROM permissions " + + "WHERE space_id = ? AND user_id = ?", + spaceId, username).orElse(""); + + if (!perms.contains(permission)) { + halt(403); + } + }; + } } From 7fc02ce02e884f9e40b649347f87b1c4dbd08c51 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:24:45 +0100 Subject: [PATCH 035/209] Listing 3.7 Add authorization filters --- .../java/com/manning/apisecurityinaction/Main.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 3db63d2..48164d4 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -58,13 +58,25 @@ public static void main(String... args) throws Exception { before("/spaces", userController::requireAuthentication); post("/spaces", spaceController::createSpace); + + before("/spaces/:spaceId/messages", + userController.requirePermission("POST", "w")); post("/spaces/:spaceId/messages", spaceController::postMessage); + + before("/spaces/:spaceId/messages/*", + userController.requirePermission("GET", "r")); get("/spaces/:spaceId/messages/:msgId", spaceController::readMessage); + + before("/spaces/:spaceId/messages", + userController.requirePermission("GET", "r")); get("/spaces/:spaceId/messages", spaceController::findMessages); var moderatorController = new ModeratorController(database); + + before("/spaces/:spaceId/messages/*", + userController.requirePermission("DELETE", "d")); delete("/spaces/:spaceId/messages/:msgId", moderatorController::deletePost); From 06f47603ff0e1f99a1521a6cece3f0bfbfaab9b7 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:27:34 +0100 Subject: [PATCH 036/209] Listing 3.8 Adding users to a space --- .../controller/SpaceController.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index ef9627d..b8db8df 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -117,6 +117,26 @@ public JSONArray findMessages(Request request, Response response) { .collect(Collectors.toList())); } + public JSONObject addMember(Request request, Response response) { + var json = new JSONObject(request.body()); + var spaceId = Long.parseLong(request.params(":spaceId")); + var userToAdd = json.getString("username"); + var perms = json.getString("permissions"); + + if (!perms.matches("r?w?d?")) { + throw new IllegalArgumentException("invalid permissions"); + } + + database.updateUnique( + "INSERT INTO permissions(space_id, user_id, perms) " + + "VALUES(?, ?, ?)", spaceId, userToAdd, perms); + + response.status(200); + return new JSONObject() + .put("username", userToAdd) + .put("permissions", perms); + } + public static class Message { private final long spaceId; private final long msgId; From ff20f66f9e753580cc1b30b509af440c308fe187 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:33:57 +0100 Subject: [PATCH 037/209] Avoid integrity constraint error in schema --- natter-api/src/main/resources/schema.sql | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 0aa165e..ed41295 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -2,13 +2,6 @@ CREATE TABLE users( user_id VARCHAR(30) PRIMARY KEY, pw_hash VARCHAR(255) NOT NULL ); -CREATE TABLE permissions( - space_id INT NOT NULL REFERENCES spaces(space_id), - user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), - perms VARCHAR(3) NOT NULL, - PRIMARY KEY (space_id, user_id) -); - CREATE TABLE spaces( space_id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, @@ -36,6 +29,14 @@ CREATE TABLE audit_log( ); CREATE SEQUENCE audit_id_seq; +CREATE TABLE permissions( + space_id INT NOT NULL REFERENCES spaces(space_id), + user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), + perms VARCHAR(3) NOT NULL, + PRIMARY KEY (space_id, user_id) +); + + CREATE USER natter_api_user PASSWORD 'password'; GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; GRANT DELETE ON messages TO natter_api_user; From 41168cc080f862b2fd42f799a836c6f2d23d9ae5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:34:15 +0100 Subject: [PATCH 038/209] Wire up addMember operation --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 48164d4..676eadb 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -72,6 +72,10 @@ public static void main(String... args) throws Exception { userController.requirePermission("GET", "r")); get("/spaces/:spaceId/messages", spaceController::findMessages); + before("/spaces/:spaceId/members", + userController.requirePermission("POST", "r")); + post("/spaces/:spaceId/members", spaceController::addMember); + var moderatorController = new ModeratorController(database); From 45da5af14350df41becd90c5bdade5de20e4f0e3 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 15:34:37 +0100 Subject: [PATCH 039/209] Avoid privilege escalation attack --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 676eadb..a765edf 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -73,7 +73,7 @@ public static void main(String... args) throws Exception { get("/spaces/:spaceId/messages", spaceController::findMessages); before("/spaces/:spaceId/members", - userController.requirePermission("POST", "r")); + userController.requirePermission("POST", "rwd")); post("/spaces/:spaceId/members", spaceController::addMember); var moderatorController = From eca74f1db4cf0bfdb1bebae167f491939b6c71a1 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:14:50 +0100 Subject: [PATCH 040/209] Listing 4.1 Calling the Natter API from JavaScript --- .../src/main/resources/public/natter.js | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 natter-api/src/main/resources/public/natter.js diff --git a/natter-api/src/main/resources/public/natter.js b/natter-api/src/main/resources/public/natter.js new file mode 100644 index 0000000..7ce422e --- /dev/null +++ b/natter-api/src/main/resources/public/natter.js @@ -0,0 +1,22 @@ +const apiUrl = 'https://localhost:4567'; + +function createSpace(name, owner) { + let data = {name: name, owner: owner}; + + fetch(apiUrl + '/spaces', { + method: 'POST', + credentials: 'include', + body: JSON.stringify(data), + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => { + if (response.ok) { + return response.json(); + } else { + throw Error(response.statusText); + } + }) + .then(json => console.log('Created space: ', json.name, json.uri)) + .catch(error => console.error('Error: ', error));} From ba11bad0b91dae13197a29880aab16e4c9b9d33e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:15:36 +0100 Subject: [PATCH 041/209] Listing 4.2 The Natter HTML UI --- .../src/main/resources/public/natter.html | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 natter-api/src/main/resources/public/natter.html diff --git a/natter-api/src/main/resources/public/natter.html b/natter-api/src/main/resources/public/natter.html new file mode 100644 index 0000000..ec99756 --- /dev/null +++ b/natter-api/src/main/resources/public/natter.html @@ -0,0 +1,21 @@ + + + + Natter! + + + + +

Create Space

+
+ + + +
+ + From 75ac9751a127be1fbfd83e8784beb271776f6fcf Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:16:21 +0100 Subject: [PATCH 042/209] Listing 4.3 Intercepting form submission --- natter-api/src/main/resources/public/natter.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/natter-api/src/main/resources/public/natter.js b/natter-api/src/main/resources/public/natter.js index 7ce422e..4a763c2 100644 --- a/natter-api/src/main/resources/public/natter.js +++ b/natter-api/src/main/resources/public/natter.js @@ -20,3 +20,19 @@ function createSpace(name, owner) { }) .then(json => console.log('Created space: ', json.name, json.uri)) .catch(error => console.error('Error: ', error));} + +window.addEventListener('load', function(e) { + document.getElementById('createSpace') + .addEventListener('submit', processFormSubmit); +}); + +function processFormSubmit(e) { + e.preventDefault(); + + let spaceName = document.getElementById('spaceName').value; + let owner = document.getElementById('owner').value; + + createSpace(spaceName, owner); + + return false; +} From 854b6f3fcb0f02779b8e8362e1aad30f35a34761 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:18:18 +0100 Subject: [PATCH 043/209] Serve HTML static files from the same origin as the API --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 1 + 1 file changed, 1 insertion(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index a765edf..dc217d4 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -18,6 +18,7 @@ public class Main { public static void main(String... args) throws Exception { + Spark.staticFiles.location("/public"); secure("localhost.p12", "changeit", null, null); var datasource = JdbcConnectionPool.create( "jdbc:h2:mem:natter", "natter", "password"); From cc83b3026c3ef2fd23659186baa83849be7050b3 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:24:30 +0100 Subject: [PATCH 044/209] Listing 4.4 CORS filter --- .../apisecurityinaction/CorsFilter.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java new file mode 100644 index 0000000..7393bd0 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java @@ -0,0 +1,41 @@ +package com.manning.apisecurityinaction; + +import static spark.Spark.halt; + +import java.util.Set; + +import spark.*; + +class CorsFilter implements Filter { + private final Set allowedOrigins; + + CorsFilter(Set allowedOrigins) { + this.allowedOrigins = allowedOrigins; + } + + @Override + public void handle(Request request, Response response) { + var origin = request.headers("Origin"); + if (origin != null && allowedOrigins.contains(origin)) { + response.header("Access-Control-Allow-Origin", origin); + response.header("Access-Control-Allow-Credentials", + "true"); + } + + if (isPreflightRequest(request)) { + if (origin == null || !allowedOrigins.contains(origin)) { + halt(403); + } + response.header("Access-Control-Allow-Headers", + "Content-Type"); + response.header("Access-Control-Allow-Methods", + "GET, POST, DELETE"); + halt(204); + } + } + + private boolean isPreflightRequest(Request request) { + return "OPTIONS".equals(request.requestMethod()) && + request.headers().contains("Access-Control-Request-Method"); + } +} From 217ea804d945edcfb743c1ae6a9c66f990df3cd5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:25:17 +0100 Subject: [PATCH 045/209] Wire up CORS filter --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index dc217d4..978ff07 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -4,6 +4,7 @@ import java.nio.file.*; import java.sql.Connection; +import java.util.Set; import org.dalesbred.Database; import org.dalesbred.result.EmptyResultException; @@ -38,6 +39,8 @@ public static void main(String... args) throws Exception { } }); + before(new CorsFilter(Set.of("http://localhost:9999"))); + before(((request, response) -> { if (request.requestMethod().equals("POST") && !"application/json".equals(request.contentType())) { From 52c2b951745c30c88bd67594b3131d7b7222e326 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:31:59 +0100 Subject: [PATCH 046/209] Listing 4.5 Producing a session cookie --- .../com/manning/apisecurityinaction/Main.java | 2 + .../controller/SessionController.java | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 978ff07..92d19e5 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -56,6 +56,8 @@ public static void main(String... args) throws Exception { before(auditController::auditRequestStart); afterAfter(auditController::auditRequestEnd); + var sessionController = new SessionController(database); + post("/sessions", sessionController::login); get("/logs", auditController::readAuditLog); post("/users", userController::registerUser); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java new file mode 100644 index 0000000..46a1d70 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java @@ -0,0 +1,39 @@ +package com.manning.apisecurityinaction.controller; + +import org.dalesbred.Database; +import org.json.JSONObject; + +import com.lambdaworks.crypto.SCryptUtil; + +import spark.*; + +public class SessionController { + + private final Database database; + + public SessionController(Database database) { + this.database = database; + } + + public JSONObject login(Request request, Response response) { + var json = new JSONObject(request.body()); + var username = json.getString("username"); + var password = json.getString("password"); + + var hash = database.findOptional(String.class, + "SELECT pw_hash FROM users WHERE user_id = ?", username); + + if (hash.isPresent() && + SCryptUtil.check(password, hash.get())) { + + // WARNING: the next line contains a security bug! + Session session = request.session(true); + session.attribute("username", username); + + response.status(200); + return new JSONObject(); + } + throw new IllegalArgumentException( + "invalid username or password"); + } +} From 9def0e0e8e0eb775f270c8499af43c1c5dc320cc Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:38:40 +0100 Subject: [PATCH 047/209] Listing 4.6 Avoid session fixation attacks --- .../apisecurityinaction/controller/SessionController.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java index 46a1d70..5460dd9 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java @@ -26,8 +26,11 @@ public JSONObject login(Request request, Response response) { if (hash.isPresent() && SCryptUtil.check(password, hash.get())) { - // WARNING: the next line contains a security bug! - Session session = request.session(true); + var session = request.session(false); + if (session != null) { + session.invalidate(); + } + session = request.session(true); session.attribute("username", username); response.status(200); From ede06655b04e97ea233eba74cca0c8bffad96874 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:40:03 +0100 Subject: [PATCH 048/209] Listing 4.7 Validating a session cookie --- .../controller/SessionController.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java index 5460dd9..64d44a1 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java @@ -39,4 +39,19 @@ public JSONObject login(Request request, Response response) { throw new IllegalArgumentException( "invalid username or password"); } + + public void validate(Request request, Response response) { + var session = request.session(false); + if (session == null) { + return; + } + + var username = session.attribute("username"); + if (username == null) { + session.invalidate(); + return; + } + + request.attribute("subject", username); + } } From 4e6e622094a19bf0e86887245df9b7abf571caa8 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:41:06 +0100 Subject: [PATCH 049/209] Wire up session validation --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 92d19e5..faab4c3 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -30,6 +30,7 @@ public static void main(String... args) throws Exception { var database = Database.forDataSource(datasource); var spaceController = new SpaceController(database); var userController = new UserController(database); + var sessionController = new SessionController(database); var rateLimiter = RateLimiter.create(2.0d); @@ -51,12 +52,12 @@ public static void main(String... args) throws Exception { })); before(userController::authenticate); + before(sessionController::validate); var auditController = new AuditController(database); before(auditController::auditRequestStart); afterAfter(auditController::auditRequestEnd); - var sessionController = new SessionController(database); post("/sessions", sessionController::login); get("/logs", auditController::readAuditLog); From f202947614ce5845eea266c40c31db32f6bfe3c5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:42:22 +0100 Subject: [PATCH 050/209] Listing 4.8 Calling the login endpoint from JavaScript --- natter-api/src/main/resources/public/login.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 natter-api/src/main/resources/public/login.js diff --git a/natter-api/src/main/resources/public/login.js b/natter-api/src/main/resources/public/login.js new file mode 100644 index 0000000..9669e57 --- /dev/null +++ b/natter-api/src/main/resources/public/login.js @@ -0,0 +1,33 @@ +const apiUrl = 'https://localhost:4567'; + +function login(username, password) { + let data = {username, password}; + + fetch(apiUrl + '/sessions', { + method: 'POST', + body: JSON.stringify(data), + credentials: "include", + headers: { + 'Content-Type': 'application/json' + } + }) + .then(res => { + if (res.ok) window.location.replace('/natter.html'); + }) + .catch(error => console.error('Error logging in: ', error)); +} + +window.addEventListener('load', function(e) { + document.getElementById('login') + .addEventListener('submit', processLoginSubmit); +}); + +function processLoginSubmit(e) { + e.preventDefault(); + + let username = document.getElementById('username').value; + let password = document.getElementById('password').value; + + login(username, password); + return false; +} From b6a44bc2241b63ede12a292f1504e4929707d9d3 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:43:04 +0100 Subject: [PATCH 051/209] Listing 4.9 The login form HTML --- .../src/main/resources/public/login.html | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 natter-api/src/main/resources/public/login.html diff --git a/natter-api/src/main/resources/public/login.html b/natter-api/src/main/resources/public/login.html new file mode 100644 index 0000000..5b507ff --- /dev/null +++ b/natter-api/src/main/resources/public/login.html @@ -0,0 +1,22 @@ + + + + Natter! + + + + +

Login

+
+ + + +
+ + From a57a2977a6bb93699812ca29fbb57981f27a6e8b Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:44:30 +0100 Subject: [PATCH 052/209] Listing 4.10 Redirecting to the login page --- natter-api/src/main/resources/public/natter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/natter-api/src/main/resources/public/natter.js b/natter-api/src/main/resources/public/natter.js index 4a763c2..a67520d 100644 --- a/natter-api/src/main/resources/public/natter.js +++ b/natter-api/src/main/resources/public/natter.js @@ -14,6 +14,8 @@ function createSpace(name, owner) { .then(response => { if (response.ok) { return response.json(); + } else if (response.status === 401) { + window.location.replace('/login.html'); } else { throw Error(response.statusText); } From c8a361720650ec8572b416a29a73eb90893d2a45 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:45:07 +0100 Subject: [PATCH 053/209] Listing 4.11 Removing the WWW-Authenticate challenge --- .../manning/apisecurityinaction/controller/UserController.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 2555b6c..41b1b0c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -78,8 +78,6 @@ public void authenticate(Request request, Response response) { public void requireAuthentication(Request request, Response response) { if (request.attribute("subject") == null) { - response.header("WWW-Authenticate", - "Basic realm=\"/\", charset=\"UTF-8\""); halt(401); } } From 8ff9a46d99f55983337d23c3728e3ef2bd5902a5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:48:06 +0100 Subject: [PATCH 054/209] Listing 4.12 Preventing Flash-based CSRF bypass --- .../java/com/manning/apisecurityinaction/Main.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index faab4c3..cd33bef 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -51,6 +51,17 @@ public static void main(String... args) throws Exception { } })); + before((request, response) -> { + var requestedWith = request.headers("X-Requested-With"); + if (requestedWith == null || + requestedWith.toLowerCase().startsWith("shockwaveflash")) { + halt(403, new JSONObject().put( + "error", "Request must contain X-Requested-With header" + ).toString()); + } + }); + + before(userController::authenticate); before(sessionController::validate); From d2be221d1273fb2eb1b6173fecded80e56b2c780 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:49:50 +0100 Subject: [PATCH 055/209] Listing 4.13 CSRF protection filter --- .../apisecurityinaction/CsrfFilter.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/CsrfFilter.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CsrfFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CsrfFilter.java new file mode 100644 index 0000000..e270dc7 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/CsrfFilter.java @@ -0,0 +1,55 @@ +package com.manning.apisecurityinaction; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static spark.Spark.halt; + +import java.security.*; +import java.util.*; + +import org.json.JSONObject; + +import spark.*; + +public class CsrfFilter implements Filter { + private static final String HEADER = "X-CSRF-Token"; + private static final Base64.Decoder DECODER = + Base64.getUrlDecoder(); + private static final Set SAFE_METHODS = + Set.of("GET", "HEAD", "OPTIONS"); + @Override + public void handle(Request request, Response response) { + + if (SAFE_METHODS.contains(request.requestMethod())) { + return; + } + + var session = request.session(false); + if (session == null) { + return; + } + + var csrfToken = request.headers(HEADER); + + if (!validate(session, csrfToken)) { + halt(401, new JSONObject().put( + "error", "Missing " + HEADER + " header" + ).toString()); + } + } + + public static boolean validate(Session session, String csrfToken) { + var expected = hash(session.id()); + var provided = csrfToken == null ? null : DECODER.decode(csrfToken); + + return MessageDigest.isEqual(expected, provided); + } + + public static byte[] hash(String cookie) { + try { + var hashFunction = MessageDigest.getInstance("SHA-256"); + return hashFunction.digest(cookie.getBytes(UTF_8)); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } +} From 36b2f8ecd292fb2a4b9c61f2c30d9c635e4b4753 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:51:29 +0100 Subject: [PATCH 056/209] Listing 4.14 Returning the anti-CSRF token after login --- .../controller/SessionController.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java index 64d44a1..271b082 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java @@ -1,13 +1,19 @@ package com.manning.apisecurityinaction.controller; +import java.util.Base64; + import org.dalesbred.Database; import org.json.JSONObject; import com.lambdaworks.crypto.SCryptUtil; +import com.manning.apisecurityinaction.CsrfFilter; import spark.*; public class SessionController { + private static final Base64.Encoder ENCODER = + Base64.getUrlEncoder().withoutPadding(); + private final Database database; @@ -33,8 +39,11 @@ public JSONObject login(Request request, Response response) { session = request.session(true); session.attribute("username", username); + var csrfToken = ENCODER.encodeToString( + CsrfFilter.hash(session.id())); + response.status(200); - return new JSONObject(); + return new JSONObject().put("token", csrfToken); } throw new IllegalArgumentException( "invalid username or password"); From c18d0a94ce69ba4d0cb39daebeecd19a369d12ab Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:53:23 +0100 Subject: [PATCH 057/209] Listing 4.15 Storing the anti-CSRF token in localStorage --- natter-api/src/main/resources/public/login.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/resources/public/login.js b/natter-api/src/main/resources/public/login.js index 9669e57..ae5fe0c 100644 --- a/natter-api/src/main/resources/public/login.js +++ b/natter-api/src/main/resources/public/login.js @@ -12,7 +12,12 @@ function login(username, password) { } }) .then(res => { - if (res.ok) window.location.replace('/natter.html'); + if (res.ok) { + res.json().then(json => { + localStorage.setItem("csrf", json.token); + window.location.replace('/natter.html'); + }); + } }) .catch(error => console.error('Error logging in: ', error)); } From 2030443a638303df99cc60260f7425f80e3c4d03 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:55:17 +0100 Subject: [PATCH 058/209] Listing 4.16 Passing the X-CSRF-Token header --- natter-api/src/main/resources/public/natter.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/resources/public/natter.js b/natter-api/src/main/resources/public/natter.js index a67520d..691735f 100644 --- a/natter-api/src/main/resources/public/natter.js +++ b/natter-api/src/main/resources/public/natter.js @@ -1,6 +1,11 @@ const apiUrl = 'https://localhost:4567'; function createSpace(name, owner) { + let csrfToken = localStorage.csrf; + if (!csrfToken) { + window.location.replace('/login.html'); + return; + } let data = {name: name, owner: owner}; fetch(apiUrl + '/spaces', { @@ -8,7 +13,8 @@ function createSpace(name, owner) { credentials: 'include', body: JSON.stringify(data), headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken } }) .then(response => { From 27eadbeba362c91241f8edccbd00b05d5635fdc3 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:55:59 +0100 Subject: [PATCH 059/209] Add X-CSRF-Token to CORS allowed headers --- .../main/java/com/manning/apisecurityinaction/CorsFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java index 7393bd0..a218a65 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java @@ -27,7 +27,7 @@ public void handle(Request request, Response response) { halt(403); } response.header("Access-Control-Allow-Headers", - "Content-Type"); + "Content-Type, X-CSRF-Token"); response.header("Access-Control-Allow-Methods", "GET, POST, DELETE"); halt(204); From e127a4a65c59b23f57d3ff3e5140233c51e519a3 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 14 May 2019 16:57:28 +0100 Subject: [PATCH 060/209] Remove X-Requested-With filter in favour of CsrfFilter --- .../java/com/manning/apisecurityinaction/Main.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index cd33bef..faab4c3 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -51,17 +51,6 @@ public static void main(String... args) throws Exception { } })); - before((request, response) -> { - var requestedWith = request.headers("X-Requested-With"); - if (requestedWith == null || - requestedWith.toLowerCase().startsWith("shockwaveflash")) { - halt(403, new JSONObject().put( - "error", "Request must contain X-Requested-With header" - ).toString()); - } - }); - - before(userController::authenticate); before(sessionController::validate); From 645600f3db569389912b7cac085d1b6a3de64f7f Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 21 May 2019 16:33:03 +0100 Subject: [PATCH 061/209] Update code to reflect revised chapter 4 --- natter-api/pom.xml | 2 +- .../apisecurityinaction/CorsFilter.java | 41 ------------ .../apisecurityinaction/CsrfFilter.java | 55 ---------------- .../com/manning/apisecurityinaction/Main.java | 18 +++-- .../controller/SessionController.java | 66 ------------------- .../controller/TokenController.java | 43 ++++++++++++ .../token/CookieTokenStore.java | 58 ++++++++++++++++ .../apisecurityinaction/token/TokenStore.java | 27 ++++++++ natter-api/src/main/resources/public/login.js | 10 +-- .../src/main/resources/public/natter.js | 20 ++++-- 10 files changed, 160 insertions(+), 180 deletions(-) delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/CsrfFilter.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java diff --git a/natter-api/pom.xml b/natter-api/pom.xml index cacff9f..cda8f7b 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -21,7 +21,7 @@ com.sparkjava spark-core - 2.7.2 + 2.9.0 org.json diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java deleted file mode 100644 index a218a65..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.manning.apisecurityinaction; - -import static spark.Spark.halt; - -import java.util.Set; - -import spark.*; - -class CorsFilter implements Filter { - private final Set allowedOrigins; - - CorsFilter(Set allowedOrigins) { - this.allowedOrigins = allowedOrigins; - } - - @Override - public void handle(Request request, Response response) { - var origin = request.headers("Origin"); - if (origin != null && allowedOrigins.contains(origin)) { - response.header("Access-Control-Allow-Origin", origin); - response.header("Access-Control-Allow-Credentials", - "true"); - } - - if (isPreflightRequest(request)) { - if (origin == null || !allowedOrigins.contains(origin)) { - halt(403); - } - response.header("Access-Control-Allow-Headers", - "Content-Type, X-CSRF-Token"); - response.header("Access-Control-Allow-Methods", - "GET, POST, DELETE"); - halt(204); - } - } - - private boolean isPreflightRequest(Request request) { - return "OPTIONS".equals(request.requestMethod()) && - request.headers().contains("Access-Control-Request-Method"); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CsrfFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CsrfFilter.java deleted file mode 100644 index e270dc7..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/CsrfFilter.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.manning.apisecurityinaction; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static spark.Spark.halt; - -import java.security.*; -import java.util.*; - -import org.json.JSONObject; - -import spark.*; - -public class CsrfFilter implements Filter { - private static final String HEADER = "X-CSRF-Token"; - private static final Base64.Decoder DECODER = - Base64.getUrlDecoder(); - private static final Set SAFE_METHODS = - Set.of("GET", "HEAD", "OPTIONS"); - @Override - public void handle(Request request, Response response) { - - if (SAFE_METHODS.contains(request.requestMethod())) { - return; - } - - var session = request.session(false); - if (session == null) { - return; - } - - var csrfToken = request.headers(HEADER); - - if (!validate(session, csrfToken)) { - halt(401, new JSONObject().put( - "error", "Missing " + HEADER + " header" - ).toString()); - } - } - - public static boolean validate(Session session, String csrfToken) { - var expected = hash(session.id()); - var provided = csrfToken == null ? null : DECODER.decode(csrfToken); - - return MessageDigest.isEqual(expected, provided); - } - - public static byte[] hash(String cookie) { - try { - var hashFunction = MessageDigest.getInstance("SHA-256"); - return hashFunction.digest(cookie.getBytes(UTF_8)); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index faab4c3..fa4b324 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -4,7 +4,6 @@ import java.nio.file.*; import java.sql.Connection; -import java.util.Set; import org.dalesbred.Database; import org.dalesbred.result.EmptyResultException; @@ -13,12 +12,17 @@ import com.google.common.util.concurrent.RateLimiter; import com.manning.apisecurityinaction.controller.*; +import com.manning.apisecurityinaction.token.*; import spark.*; +import spark.embeddedserver.EmbeddedServers; +import spark.embeddedserver.jetty.EmbeddedJettyFactory; public class Main { public static void main(String... args) throws Exception { + EmbeddedServers.add(EmbeddedServers.defaultIdentifier(), + new EmbeddedJettyFactory().withHttpOnly(true)); Spark.staticFiles.location("/public"); secure("localhost.p12", "changeit", null, null); var datasource = JdbcConnectionPool.create( @@ -30,7 +34,6 @@ public static void main(String... args) throws Exception { var database = Database.forDataSource(datasource); var spaceController = new SpaceController(database); var userController = new UserController(database); - var sessionController = new SessionController(database); var rateLimiter = RateLimiter.create(2.0d); @@ -40,8 +43,6 @@ public static void main(String... args) throws Exception { } }); - before(new CorsFilter(Set.of("http://localhost:9999"))); - before(((request, response) -> { if (request.requestMethod().equals("POST") && !"application/json".equals(request.contentType())) { @@ -51,14 +52,19 @@ public static void main(String... args) throws Exception { } })); + TokenStore tokenStore = new CookieTokenStore(); + var tokenController = new TokenController(tokenStore); + before(userController::authenticate); - before(sessionController::validate); + before(tokenController::validateToken); var auditController = new AuditController(database); before(auditController::auditRequestStart); afterAfter(auditController::auditRequestEnd); - post("/sessions", sessionController::login); + before("/sessions", userController::requireAuthentication); + post("/sessions", tokenController::login); + get("/logs", auditController::readAuditLog); post("/users", userController::registerUser); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java deleted file mode 100644 index 271b082..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SessionController.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import java.util.Base64; - -import org.dalesbred.Database; -import org.json.JSONObject; - -import com.lambdaworks.crypto.SCryptUtil; -import com.manning.apisecurityinaction.CsrfFilter; - -import spark.*; - -public class SessionController { - private static final Base64.Encoder ENCODER = - Base64.getUrlEncoder().withoutPadding(); - - - private final Database database; - - public SessionController(Database database) { - this.database = database; - } - - public JSONObject login(Request request, Response response) { - var json = new JSONObject(request.body()); - var username = json.getString("username"); - var password = json.getString("password"); - - var hash = database.findOptional(String.class, - "SELECT pw_hash FROM users WHERE user_id = ?", username); - - if (hash.isPresent() && - SCryptUtil.check(password, hash.get())) { - - var session = request.session(false); - if (session != null) { - session.invalidate(); - } - session = request.session(true); - session.attribute("username", username); - - var csrfToken = ENCODER.encodeToString( - CsrfFilter.hash(session.id())); - - response.status(200); - return new JSONObject().put("token", csrfToken); - } - throw new IllegalArgumentException( - "invalid username or password"); - } - - public void validate(Request request, Response response) { - var session = request.session(false); - if (session == null) { - return; - } - - var username = session.attribute("username"); - if (username == null) { - session.invalidate(); - return; - } - - request.attribute("subject", username); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java new file mode 100644 index 0000000..9548a45 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java @@ -0,0 +1,43 @@ +package com.manning.apisecurityinaction.controller; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import org.json.JSONObject; + +import com.manning.apisecurityinaction.token.TokenStore; + +import spark.*; + +public class TokenController { + + private final TokenStore tokenStore; + + public TokenController(TokenStore tokenStore) { + this.tokenStore = tokenStore; + } + + public JSONObject login(Request request, Response response) { + String subject = request.attribute("subject"); + var expiry = Instant.now().plus(10, ChronoUnit.MINUTES); + + var token = new TokenStore.Token(expiry, subject); + var tokenId = tokenStore.create(request, token); + + response.status(201); + return new JSONObject() + .put("token", tokenId); + } + + public void validateToken(Request request, Response response) { + var tokenId = request.headers("X-CSRF-Token"); + if (tokenId == null) return; + + tokenStore.read(request, tokenId).ifPresent(token -> { + if (Instant.now().isBefore(token.expiry)) { + request.attribute("subject", token.username); + token.attributes.forEach(request::attribute); + } + }); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java new file mode 100644 index 0000000..a0f9930 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java @@ -0,0 +1,58 @@ +package com.manning.apisecurityinaction.token; + +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.util.*; + +import spark.Request; + +public class CookieTokenStore implements TokenStore { + + @Override + public String create(Request request, Token token) { + + var session = request.session(false); + if (session != null) { + session.invalidate(); + } + session = request.session(true); + + session.attribute("username", token.username); + session.attribute("expiry", token.expiry); + session.attribute("attrs", token.attributes); + + return Base64.getUrlEncoder().withoutPadding() + .encodeToString(sha256(session.id())); + } + + @Override + public Optional read(Request request, String tokenId) { + + var session = request.session(false); + if (session == null) { + return Optional.empty(); + } + + var provided = Base64.getUrlDecoder().decode(tokenId); + var computed = sha256(session.id()); + + if (!MessageDigest.isEqual(computed, provided)) { + return Optional.empty(); + } + + var token = new Token(session.attribute("expiry"), + session.attribute("username")); + token.attributes.putAll(session.attribute("attrs")); + + return Optional.of(token); + } + + private static byte[] sha256(String tokenId) { + try { + var sha256 = MessageDigest.getInstance("SHA-256"); + return sha256.digest(tokenId.getBytes(StandardCharsets.UTF_8)); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java new file mode 100644 index 0000000..2f74dee --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java @@ -0,0 +1,27 @@ +package com.manning.apisecurityinaction.token; + +import java.time.Instant; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import spark.Request; + +public interface TokenStore { + + String create(Request request, Token token); + Optional read(Request request, String tokenId); + + class Token { + public final Instant expiry; + public final String username; + public final Map attributes; + + public Token(Instant expiry, String username) { + this.expiry = expiry; + this.username = username; + this.attributes = new ConcurrentHashMap<>(); + } + } + +} diff --git a/natter-api/src/main/resources/public/login.js b/natter-api/src/main/resources/public/login.js index ae5fe0c..6acc059 100644 --- a/natter-api/src/main/resources/public/login.js +++ b/natter-api/src/main/resources/public/login.js @@ -1,20 +1,20 @@ const apiUrl = 'https://localhost:4567'; function login(username, password) { - let data = {username, password}; + let credentials = 'Basic ' + btoa(username + ':' + password); fetch(apiUrl + '/sessions', { method: 'POST', - body: JSON.stringify(data), - credentials: "include", headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'Authorization': credentials } }) .then(res => { if (res.ok) { res.json().then(json => { - localStorage.setItem("csrf", json.token); + document.cookie = 'csrfToken=' + json.token + + ';Secure;SameSite=strict'; window.location.replace('/natter.html'); }); } diff --git a/natter-api/src/main/resources/public/natter.js b/natter-api/src/main/resources/public/natter.js index 691735f..f7b6b95 100644 --- a/natter-api/src/main/resources/public/natter.js +++ b/natter-api/src/main/resources/public/natter.js @@ -1,12 +1,8 @@ const apiUrl = 'https://localhost:4567'; function createSpace(name, owner) { - let csrfToken = localStorage.csrf; - if (!csrfToken) { - window.location.replace('/login.html'); - return; - } let data = {name: name, owner: owner}; + let csrfToken = getCookie('csrfToken'); fetch(apiUrl + '/spaces', { method: 'POST', @@ -27,7 +23,19 @@ function createSpace(name, owner) { } }) .then(json => console.log('Created space: ', json.name, json.uri)) - .catch(error => console.error('Error: ', error));} + .catch(error => console.error('Error: ', error)); +} + +function getCookie(cookieName) { + var cookieValue = document.cookie.split(';') + .map(item => item.split('=') + .map(x => decodeURIComponent(x.trim()))) + .filter(item => item[0] === cookieName)[0] + + if (cookieValue) { + return cookieValue[1]; + } +} window.addEventListener('load', function(e) { document.getElementById('createSpace') From d6df75bbd0a2a6caadb85b0dec44ccd337dae731 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 2 Jun 2019 11:30:14 +0100 Subject: [PATCH 062/209] Add logout --- .../com/manning/apisecurityinaction/Main.java | 1 + .../controller/TokenController.java | 11 +++++++++++ .../token/CookieTokenStore.java | 15 +++++++++++++++ .../apisecurityinaction/token/TokenStore.java | 1 + 4 files changed, 28 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index fa4b324..9a13f39 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -64,6 +64,7 @@ public static void main(String... args) throws Exception { before("/sessions", userController::requireAuthentication); post("/sessions", tokenController::login); + delete("/sessions", tokenController::logout); get("/logs", auditController::readAuditLog); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java index 9548a45..fc243f8 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java @@ -40,4 +40,15 @@ public void validateToken(Request request, Response response) { } }); } + + public JSONObject logout(Request request, Response response) { + var tokenId = request.headers("X-CSRF-Token"); + if (tokenId == null) + throw new IllegalArgumentException("missing token header"); + + tokenStore.revoke(request, tokenId); + + response.status(200); + return new JSONObject(); + } } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java index a0f9930..e82bb4e 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java @@ -47,6 +47,21 @@ public Optional read(Request request, String tokenId) { return Optional.of(token); } + @Override + public void revoke(Request request, String tokenId) { + var session = request.session(false); + if (session == null) return; + + var provided = Base64.getUrlDecoder().decode(tokenId); + var computed = sha256(session.id()); + + if (!MessageDigest.isEqual(computed, provided)) { + return; + } + + session.invalidate(); + } + private static byte[] sha256(String tokenId) { try { var sha256 = MessageDigest.getInstance("SHA-256"); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java index 2f74dee..02e85ea 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java @@ -11,6 +11,7 @@ public interface TokenStore { String create(Request request, Token token); Optional read(Request request, String tokenId); + void revoke(Request request, String tokenId); class Token { public final Instant expiry; From 0d6bf791daa8203f4fc11c359a3f32f0c7da79a2 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 22 May 2019 10:53:14 +0100 Subject: [PATCH 063/209] Allow running on a different port --- .../com/manning/apisecurityinaction/Main.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 9a13f39..8ad8079 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -1,23 +1,22 @@ package com.manning.apisecurityinaction; -import static spark.Spark.*; - -import java.nio.file.*; -import java.sql.Connection; - +import com.google.common.util.concurrent.RateLimiter; +import com.manning.apisecurityinaction.controller.*; +import com.manning.apisecurityinaction.token.*; import org.dalesbred.Database; import org.dalesbred.result.EmptyResultException; import org.h2.jdbcx.JdbcConnectionPool; import org.json.*; - -import com.google.common.util.concurrent.RateLimiter; -import com.manning.apisecurityinaction.controller.*; -import com.manning.apisecurityinaction.token.*; - import spark.*; import spark.embeddedserver.EmbeddedServers; import spark.embeddedserver.jetty.EmbeddedJettyFactory; +import java.nio.file.*; +import java.sql.Connection; + +import static spark.Service.SPARK_DEFAULT_PORT; +import static spark.Spark.*; + public class Main { public static void main(String... args) throws Exception { @@ -25,6 +24,9 @@ public static void main(String... args) throws Exception { new EmbeddedJettyFactory().withHttpOnly(true)); Spark.staticFiles.location("/public"); secure("localhost.p12", "changeit", null, null); + port(args.length > 0 ? Integer.parseInt(args[0]) + : SPARK_DEFAULT_PORT); + var datasource = JdbcConnectionPool.create( "jdbc:h2:mem:natter", "natter", "password"); createTables(datasource.getConnection()); From f92c92fa78ae5aa9e4a98fedf582cb06d25a2e13 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 22 May 2019 11:02:22 +0100 Subject: [PATCH 064/209] Listing 5.1 CORS filter --- .../apisecurityinaction/CorsFilter.java | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java new file mode 100644 index 0000000..b5b85b5 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java @@ -0,0 +1,43 @@ +package com.manning.apisecurityinaction; + +import spark.*; + +import java.util.Set; + +import static spark.Spark.halt; + +class CorsFilter implements Filter { + private final Set allowedOrigins; + + CorsFilter(Set allowedOrigins) { + this.allowedOrigins = allowedOrigins; + } + + @Override + public void handle(Request request, Response response) { + var origin = request.headers("Origin"); + if (origin != null && allowedOrigins.contains(origin)) { + response.header("Access-Control-Allow-Origin", origin); + response.header("Access-Control-Allow-Credentials", + "true"); + response.header("Vary", "Origin"); + } + + if (isPreflightRequest(request)) { + if (origin == null || !allowedOrigins.contains(origin)) { + halt(403); + } + + response.header("Access-Control-Allow-Headers", + "Content-Type, X-CSRF-Token"); + response.header("Access-Control-Allow-Methods", + "GET, POST, DELETE"); + halt(204); + } + } + + private boolean isPreflightRequest(Request request) { + return "OPTIONS".equals(request.requestMethod()) && + request.headers().contains("Access-Control-Request-Method"); + } +} From 01e29b71b2835312a5128b0abfd4e85e59997d0e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 22 May 2019 13:42:33 +0100 Subject: [PATCH 065/209] Enable the CORS filter --- .../main/java/com/manning/apisecurityinaction/CorsFilter.java | 2 +- .../src/main/java/com/manning/apisecurityinaction/Main.java | 3 ++- natter-api/src/main/resources/public/login.js | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java index b5b85b5..e563b99 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java @@ -29,7 +29,7 @@ public void handle(Request request, Response response) { } response.header("Access-Control-Allow-Headers", - "Content-Type, X-CSRF-Token"); + "Content-Type, Authorization, X-CSRF-Token"); response.header("Access-Control-Allow-Methods", "GET, POST, DELETE"); halt(204); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 8ad8079..52d98dd 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -13,6 +13,7 @@ import java.nio.file.*; import java.sql.Connection; +import java.util.Set; import static spark.Service.SPARK_DEFAULT_PORT; import static spark.Spark.*; @@ -38,12 +39,12 @@ public static void main(String... args) throws Exception { var userController = new UserController(database); var rateLimiter = RateLimiter.create(2.0d); - before((request, response) -> { if (!rateLimiter.tryAcquire()) { halt(429); } }); + before(new CorsFilter(Set.of("https://localhost:9999"))); before(((request, response) -> { if (request.requestMethod().equals("POST") && diff --git a/natter-api/src/main/resources/public/login.js b/natter-api/src/main/resources/public/login.js index 6acc059..64a7911 100644 --- a/natter-api/src/main/resources/public/login.js +++ b/natter-api/src/main/resources/public/login.js @@ -5,6 +5,7 @@ function login(username, password) { fetch(apiUrl + '/sessions', { method: 'POST', + credentials: 'include', headers: { 'Content-Type': 'application/json', 'Authorization': credentials From e6a7e3702dcf0352f08ed951ab9fbc1f8b2b6132 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 22 May 2019 16:48:29 +0100 Subject: [PATCH 066/209] The DatabaseTokenStore --- .../com/manning/apisecurityinaction/Main.java | 2 +- .../token/DatabaseTokenStore.java | 64 +++++++++++++++++++ .../apisecurityinaction/token/TokenStore.java | 7 +- natter-api/src/main/resources/schema.sql | 9 ++- 4 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 52d98dd..595880e 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -55,7 +55,7 @@ public static void main(String... args) throws Exception { } })); - TokenStore tokenStore = new CookieTokenStore(); + TokenStore tokenStore = new DatabaseTokenStore(database); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java new file mode 100644 index 0000000..b7cec93 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java @@ -0,0 +1,64 @@ +package com.manning.apisecurityinaction.token; + +import org.dalesbred.Database; +import org.json.JSONObject; +import spark.Request; + +import java.security.SecureRandom; +import java.sql.*; +import java.util.*; + +public class DatabaseTokenStore implements TokenStore { + private final Database database; + private final SecureRandom secureRandom; + + public DatabaseTokenStore(Database database) { + this.database = database; + this.secureRandom = new SecureRandom(); + } + + private String randomId() { + var bytes = new byte[20]; + secureRandom.nextBytes(bytes); + return Base64.getUrlEncoder().withoutPadding() + .encodeToString(bytes); + } + + @Override + public String create(Request request, Token token) { + var tokenId = randomId(); + var attrs = new JSONObject(token.attributes).toString(); + + database.updateUnique("INSERT INTO " + + "tokens(token_id, user_id, expiry, attributes) " + + "VALUES(?, ?, ?, ?)", tokenId, token.username, + token.expiry, attrs); + + return tokenId; + } + + @Override + public Optional read(Request request, String tokenId) { + return database.findOptional(this::readToken, + "SELECT user_id, expiry, attributes " + + "FROM tokens WHERE token_id = ?", tokenId); + } + + @Override + public void revoke(Request request, String tokenId) { + database.update("DELETE FROM tokens WHERE token_id = ?", + tokenId); + } + + private Token readToken(ResultSet resultSet) throws SQLException { + var username = resultSet.getString(1); + var expiry = resultSet.getTimestamp(2).toInstant(); + var json = new JSONObject(resultSet.getString(3)); + + var token = new Token(expiry, username); + for (var key : json.keySet()) { + token.attributes.put(key, json.getString(key)); + } + return token; + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java index 02e85ea..647dd5d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java @@ -1,12 +1,11 @@ package com.manning.apisecurityinaction.token; +import spark.Request; + import java.time.Instant; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import spark.Request; - public interface TokenStore { String create(Request request, Token token); diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index ed41295..ba98b9a 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -36,10 +36,17 @@ CREATE TABLE permissions( PRIMARY KEY (space_id, user_id) ); +CREATE TABLE tokens( + token_id VARCHAR(30) PRIMARY KEY, + user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), + expiry TIMESTAMP NOT NULL, + attributes VARCHAR(4096) NOT NULL +); CREATE USER natter_api_user PASSWORD 'password'; GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; GRANT DELETE ON messages TO natter_api_user; GRANT SELECT, INSERT ON users TO natter_api_user; GRANT SELECT, INSERT ON audit_log TO natter_api_user; -GRANT SELECT, INSERT ON permissions TO natter_api_user; \ No newline at end of file +GRANT SELECT, INSERT ON permissions TO natter_api_user; +GRANT SELECT, INSERT, DELETE ON tokens TO natter_api_user; \ No newline at end of file From 6d86a47fc494eb10dac61715fd1132f90bb9b8d0 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 22 May 2019 19:25:09 +0100 Subject: [PATCH 067/209] Bearer authentication scheme --- .../controller/TokenController.java | 21 ++++++++++++------- .../controller/UserController.java | 1 + 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java index fc243f8..94c1a42 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java @@ -1,14 +1,12 @@ package com.manning.apisecurityinaction.controller; -import java.time.Instant; -import java.time.temporal.ChronoUnit; - -import org.json.JSONObject; - import com.manning.apisecurityinaction.token.TokenStore; - +import org.json.JSONObject; import spark.*; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + public class TokenController { private final TokenStore tokenStore; @@ -30,13 +28,20 @@ public JSONObject login(Request request, Response response) { } public void validateToken(Request request, Response response) { - var tokenId = request.headers("X-CSRF-Token"); - if (tokenId == null) return; + var tokenId = request.headers("Authorization"); + if (tokenId == null || !tokenId.startsWith("Bearer ")) { + return; + } + tokenId = tokenId.substring(7); tokenStore.read(request, tokenId).ifPresent(token -> { if (Instant.now().isBefore(token.expiry)) { request.attribute("subject", token.username); token.attributes.forEach(request::attribute); + } else { + response.header("WWW-Authenticate", + "Bearer error=\"invalid_token\"," + + "error_description=\"Expired\""); } }); } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 41b1b0c..825f3a0 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -78,6 +78,7 @@ public void authenticate(Request request, Response response) { public void requireAuthentication(Request request, Response response) { if (request.attribute("subject") == null) { + response.header("WWW-Authenticate", "Bearer"); halt(401); } } From 7be9d0ef3caea76301cf21d95e5b9fb84f8c6591 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 2 Jun 2019 18:14:48 +0100 Subject: [PATCH 068/209] switch to bearer --- .../apisecurityinaction/controller/TokenController.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java index 94c1a42..52bbd98 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java @@ -47,9 +47,11 @@ public void validateToken(Request request, Response response) { } public JSONObject logout(Request request, Response response) { - var tokenId = request.headers("X-CSRF-Token"); - if (tokenId == null) + var tokenId = request.headers("Authorization"); + if (tokenId == null || !tokenId.startsWith("Bearer ")) { throw new IllegalArgumentException("missing token header"); + } + tokenId = tokenId.substring(7); tokenStore.revoke(request, tokenId); From d5c9438e5daba199e61734d074e6c8f6622f1cdb Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 22 May 2019 19:50:31 +0100 Subject: [PATCH 069/209] Delete expired tokens --- .../token/DatabaseTokenStore.java | 15 +++++++++++++++ natter-api/src/main/resources/schema.sql | 1 + 2 files changed, 16 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java index b7cec93..3f12acb 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java @@ -2,19 +2,28 @@ import org.dalesbred.Database; import org.json.JSONObject; +import org.slf4j.*; import spark.Request; import java.security.SecureRandom; import java.sql.*; import java.util.*; +import java.util.concurrent.*; public class DatabaseTokenStore implements TokenStore { + private static final Logger logger = + LoggerFactory.getLogger(DatabaseTokenStore.class); + private final Database database; private final SecureRandom secureRandom; public DatabaseTokenStore(Database database) { this.database = database; this.secureRandom = new SecureRandom(); + + Executors.newSingleThreadScheduledExecutor() + .scheduleWithFixedDelay(this::deleteExpiredTokens, + 10, 10, TimeUnit.MINUTES); } private String randomId() { @@ -61,4 +70,10 @@ private Token readToken(ResultSet resultSet) throws SQLException { } return token; } + + private void deleteExpiredTokens() { + var deleted = database.update( + "DELETE FROM tokens WHERE expiry < current_timestamp"); + logger.info("Deleted {} expired tokens", deleted); + } } diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index ba98b9a..7533c95 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -42,6 +42,7 @@ CREATE TABLE tokens( expiry TIMESTAMP NOT NULL, attributes VARCHAR(4096) NOT NULL ); +CREATE INDEX expired_token_idx ON tokens(expiry); CREATE USER natter_api_user PASSWORD 'password'; GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; From 4a58b4c7ca672effa4170fa6a89fc6cb111d6c65 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 2 Jun 2019 17:57:36 +0100 Subject: [PATCH 070/209] Switch to scheduleAtFixedRate --- .../manning/apisecurityinaction/token/DatabaseTokenStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java index 3f12acb..7b06292 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java @@ -22,7 +22,7 @@ public DatabaseTokenStore(Database database) { this.secureRandom = new SecureRandom(); Executors.newSingleThreadScheduledExecutor() - .scheduleWithFixedDelay(this::deleteExpiredTokens, + .scheduleAtFixedRate(this::deleteExpiredTokens, 10, 10, TimeUnit.MINUTES); } From a89bbdc157625e169aefa6f78802f9b5d66adf86 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 22 May 2019 20:59:52 +0100 Subject: [PATCH 071/209] Switch to Bearer auth in the UI --- natter-api/src/main/resources/public/login.js | 2 +- natter-api/src/main/resources/public/natter.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/natter-api/src/main/resources/public/login.js b/natter-api/src/main/resources/public/login.js index 64a7911..331e763 100644 --- a/natter-api/src/main/resources/public/login.js +++ b/natter-api/src/main/resources/public/login.js @@ -14,7 +14,7 @@ function login(username, password) { .then(res => { if (res.ok) { res.json().then(json => { - document.cookie = 'csrfToken=' + json.token + + document.cookie = 'token=' + json.token + ';Secure;SameSite=strict'; window.location.replace('/natter.html'); }); diff --git a/natter-api/src/main/resources/public/natter.js b/natter-api/src/main/resources/public/natter.js index f7b6b95..f6d87d1 100644 --- a/natter-api/src/main/resources/public/natter.js +++ b/natter-api/src/main/resources/public/natter.js @@ -2,15 +2,14 @@ const apiUrl = 'https://localhost:4567'; function createSpace(name, owner) { let data = {name: name, owner: owner}; - let csrfToken = getCookie('csrfToken'); + let token = getCookie('token'); fetch(apiUrl + '/spaces', { method: 'POST', - credentials: 'include', body: JSON.stringify(data), headers: { 'Content-Type': 'application/json', - 'X-CSRF-Token': csrfToken + 'Authorization': 'Bearer ' + token } }) .then(response => { From f29b97028374227422ea49f5f6619b5808a45e0e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 22 May 2019 21:23:56 +0100 Subject: [PATCH 072/209] Use localStorage for tokens --- natter-api/src/main/resources/public/login.js | 3 +-- natter-api/src/main/resources/public/natter.js | 13 +------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/natter-api/src/main/resources/public/login.js b/natter-api/src/main/resources/public/login.js index 331e763..747357b 100644 --- a/natter-api/src/main/resources/public/login.js +++ b/natter-api/src/main/resources/public/login.js @@ -14,8 +14,7 @@ function login(username, password) { .then(res => { if (res.ok) { res.json().then(json => { - document.cookie = 'token=' + json.token + - ';Secure;SameSite=strict'; + localStorage.setItem('token', json.token); window.location.replace('/natter.html'); }); } diff --git a/natter-api/src/main/resources/public/natter.js b/natter-api/src/main/resources/public/natter.js index f6d87d1..a94a987 100644 --- a/natter-api/src/main/resources/public/natter.js +++ b/natter-api/src/main/resources/public/natter.js @@ -2,7 +2,7 @@ const apiUrl = 'https://localhost:4567'; function createSpace(name, owner) { let data = {name: name, owner: owner}; - let token = getCookie('token'); + let token = localStorage.getItem('token'); fetch(apiUrl + '/spaces', { method: 'POST', @@ -25,17 +25,6 @@ function createSpace(name, owner) { .catch(error => console.error('Error: ', error)); } -function getCookie(cookieName) { - var cookieValue = document.cookie.split(';') - .map(item => item.split('=') - .map(x => decodeURIComponent(x.trim()))) - .filter(item => item[0] === cookieName)[0] - - if (cookieValue) { - return cookieValue[1]; - } -} - window.addEventListener('load', function(e) { document.getElementById('createSpace') .addEventListener('submit', processFormSubmit); From c5d1ea62760b6a189a6e55ecfb5a96689b99ee9f Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 22 May 2019 22:06:07 +0100 Subject: [PATCH 073/209] Tighten up the CORS settings --- .../main/java/com/manning/apisecurityinaction/CorsFilter.java | 4 +--- natter-api/src/main/resources/public/login.js | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java index e563b99..95e578e 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java @@ -18,8 +18,6 @@ public void handle(Request request, Response response) { var origin = request.headers("Origin"); if (origin != null && allowedOrigins.contains(origin)) { response.header("Access-Control-Allow-Origin", origin); - response.header("Access-Control-Allow-Credentials", - "true"); response.header("Vary", "Origin"); } @@ -29,7 +27,7 @@ public void handle(Request request, Response response) { } response.header("Access-Control-Allow-Headers", - "Content-Type, Authorization, X-CSRF-Token"); + "Content-Type, Authorization"); response.header("Access-Control-Allow-Methods", "GET, POST, DELETE"); halt(204); diff --git a/natter-api/src/main/resources/public/login.js b/natter-api/src/main/resources/public/login.js index 747357b..8583bb7 100644 --- a/natter-api/src/main/resources/public/login.js +++ b/natter-api/src/main/resources/public/login.js @@ -5,7 +5,6 @@ function login(username, password) { fetch(apiUrl + '/sessions', { method: 'POST', - credentials: 'include', headers: { 'Content-Type': 'application/json', 'Authorization': credentials From fef727a43f8a44cb02c24a15f2bb8b7bd0a8cf66 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 23 May 2019 19:49:25 +0100 Subject: [PATCH 074/209] Client-side stateless tokens --- .../com/manning/apisecurityinaction/Main.java | 3 +- .../controller/TokenController.java | 3 ++ .../token/JsonTokenStore.java | 43 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index fb12d7b..d03ac04 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -64,7 +64,8 @@ public static void main(String... args) throws Exception { keyPassword); var macKey = keyStore.getKey("hmac-key", keyPassword); - TokenStore tokenStore = new DatabaseTokenStore(database); +// TokenStore tokenStore = new DatabaseTokenStore(database); + TokenStore tokenStore = new JsonTokenStore(); tokenStore = new HmacTokenStore(tokenStore, macKey); var tokenController = new TokenController(tokenStore); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java index 52bbd98..7417b7f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java @@ -7,6 +7,8 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import static spark.Spark.halt; + public class TokenController { private final TokenStore tokenStore; @@ -42,6 +44,7 @@ public void validateToken(Request request, Response response) { response.header("WWW-Authenticate", "Bearer error=\"invalid_token\"," + "error_description=\"Expired\""); + halt(401); } }); } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java new file mode 100644 index 0000000..5535f85 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java @@ -0,0 +1,43 @@ +package com.manning.apisecurityinaction.token; + +import org.json.*; +import spark.Request; + +import java.time.Instant; +import java.util.*; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class JsonTokenStore implements TokenStore { + @Override + public String create(Request request, Token token) { + var json = new JSONObject(); + json.put("sub", token.username); + json.put("exp", token.expiry.getEpochSecond()); + json.put("attrs", token.attributes); + + var jsonString = json.toString(); + return Base64.getUrlEncoder().withoutPadding() + .encodeToString(jsonString.getBytes(UTF_8)); + } + + @Override + public Optional read(Request request, String tokenId) { + try { + var decoded = Base64.getUrlDecoder().decode(tokenId); + var json = new JSONObject(new String(decoded, UTF_8)); + var expiry = Instant.ofEpochSecond(json.getInt("exp")); + var username = json.getString("sub"); + var attrs = json.getJSONObject("attrs"); + + var token = new Token(expiry, username); + for (var key : attrs.keySet()) { + token.attributes.put(key, attrs.getString(key)); + } + + return Optional.of(token); + } catch (JSONException e) { + return Optional.empty(); + } + } +} From 2cfb52f0c113601f5550b3149dfa9f30e71e7481 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 2 Jun 2019 17:31:56 +0100 Subject: [PATCH 075/209] JsonTokenStore revocation --- .../manning/apisecurityinaction/token/JsonTokenStore.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java index 5535f85..7f86a8c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java @@ -40,4 +40,9 @@ public Optional read(Request request, String tokenId) { return Optional.empty(); } } + + @Override + public void revoke(Request request, String tokenId) { + // TODO + } } From badd9c7672b2e19d7fff3afdeb8da7d8e42ff9e0 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 24 May 2019 14:03:16 +0100 Subject: [PATCH 076/209] Add encryption to client-side tokens --- natter-api/keystore.p12 | Bin 327 -> 543 bytes .../com/manning/apisecurityinaction/Main.java | 2 + .../token/EncryptedTokenStore.java | 72 ++++++++++++++++++ .../token/JsonTokenStore.java | 6 +- 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java diff --git a/natter-api/keystore.p12 b/natter-api/keystore.p12 index 45dc68cc56d0bf3674d8d73fcf515c230e82be94..b48062c887d9cf14e2cc39c30400e2ccbbe4c58b 100644 GIT binary patch delta 279 zcmX@kG@r%RpovMEiILf$iSY^>r&gOs+jm|@cE$xwj7M3T7!Lx4_X6=wRH;=!sbxUn zMH5XgGc}%_Xkk{rhW*#`!Xkh1DG70VR*h+9n%UmmaKfGbuB4GLVJa z%PC?gAjij$$dJlV%%IDV4Wug#6yeen-wSw{n424zTN)Y|*cm7qaI&##^D#3?u`;lT zq@3_rAbLUd-&vmR{EsyfxOc=>^|FZU`tEQ2RA{P0Iz!dpVu32h@Aq;|n3x&c761Tq CGGKuK delta 125 zcmbQwa-7N3po!6$iILf$@h=;vR+~rLcV0$z#s!VDyob VZmaCGsmnbc7#AOBVrFbx006*BF8lxh diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index d03ac04..8e8bf37 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -63,9 +63,11 @@ public static void main(String... args) throws Exception { keyStore.load(new FileInputStream("keystore.p12"), keyPassword); var macKey = keyStore.getKey("hmac-key", keyPassword); + var encKey = keyStore.getKey("aes-key", keyPassword); // TokenStore tokenStore = new DatabaseTokenStore(database); TokenStore tokenStore = new JsonTokenStore(); + tokenStore = new EncryptedTokenStore(tokenStore, encKey); tokenStore = new HmacTokenStore(tokenStore, macKey); var tokenController = new TokenController(tokenStore); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java new file mode 100644 index 0000000..4898baf --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java @@ -0,0 +1,72 @@ +package com.manning.apisecurityinaction.token; + +import spark.Request; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import java.security.*; +import java.util.*; + +import static javax.crypto.Cipher.*; + +public class EncryptedTokenStore implements TokenStore { + + private final TokenStore delegate; + private final Key encryptionKey; + + private final Base64.Encoder encoder; + private final Base64.Decoder decoder; + + public EncryptedTokenStore(TokenStore delegate, Key encryptionKey) { + this.delegate = delegate; + this.encryptionKey = encryptionKey; + this.encoder = Base64.getUrlEncoder().withoutPadding(); + this.decoder = Base64.getUrlDecoder(); + } + + @Override + public String create(Request request, Token token) { + var tokenId = delegate.create(request, token); + + var nonceAndCiphertext = encrypt(encryptionKey, + decoder.decode(tokenId)); + + return encoder.encodeToString(nonceAndCiphertext[0]) + '.' + + encoder.encodeToString(nonceAndCiphertext[1]); + } + + @Override + public Optional read(Request request, String tokenId) { + var index = tokenId.indexOf('.'); + if (index == -1) { return Optional.empty(); } + + var nonce = decoder.decode(tokenId.substring(0, index)); + var encrypted = decoder.decode(tokenId.substring(index + 1)); + var decrypted = decrypt(encryptionKey, nonce, encrypted); + + return delegate.read(request, encoder.encodeToString(decrypted)); + } + + static byte[][] encrypt(Key key, byte[] message) { + try { + var cipher = Cipher.getInstance("AES/CTR/NoPadding"); + var nonce = new byte[16]; + new SecureRandom().nextBytes(nonce); + cipher.init(ENCRYPT_MODE, key, new IvParameterSpec(nonce)); + var encrypted = cipher.doFinal(message); + return new byte[][]{nonce, encrypted}; + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + static byte[] decrypt(Key key, byte[] nonce, byte[] ciphertext) { + try { + var cipher = Cipher.getInstance("AES/CTR/NoPadding"); + cipher.init(DECRYPT_MODE, key, new IvParameterSpec(nonce)); + return cipher.doFinal(ciphertext); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java index 7f86a8c..b2ac74b 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java @@ -9,6 +9,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; public class JsonTokenStore implements TokenStore { + @Override public String create(Request request, Token token) { var json = new JSONObject(); @@ -16,9 +17,9 @@ public String create(Request request, Token token) { json.put("exp", token.expiry.getEpochSecond()); json.put("attrs", token.attributes); - var jsonString = json.toString(); + var jsonBytes = json.toString().getBytes(UTF_8); return Base64.getUrlEncoder().withoutPadding() - .encodeToString(jsonString.getBytes(UTF_8)); + .encodeToString(jsonBytes); } @Override @@ -45,4 +46,5 @@ public Optional read(Request request, String tokenId) { public void revoke(Request request, String tokenId) { // TODO } + } From 6a5c8e4ff15cb4d8829a1539f3cb336d67c76580 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 2 Jun 2019 17:37:55 +0100 Subject: [PATCH 077/209] Encrypted token revocation --- .../token/EncryptedTokenStore.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java index 4898baf..eabd2f7 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java @@ -47,6 +47,18 @@ public Optional read(Request request, String tokenId) { return delegate.read(request, encoder.encodeToString(decrypted)); } + @Override + public void revoke(Request request, String tokenId) { + var index = tokenId.indexOf('.'); + if (index == -1) { return; } + + var nonce = decoder.decode(tokenId.substring(0, index)); + var encrypted = decoder.decode(tokenId.substring(index + 1)); + var decrypted = decrypt(encryptionKey, nonce, encrypted); + + delegate.revoke(request, encoder.encodeToString(decrypted)); + } + static byte[][] encrypt(Key key, byte[] message) { try { var cipher = Cipher.getInstance("AES/CTR/NoPadding"); From 6f070e9aa6c27405c9283f681dc13fac138501b6 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 24 May 2019 20:55:41 +0100 Subject: [PATCH 078/209] Move to ChaCha20-Poly1305 authenticated encryption --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 2 -- .../apisecurityinaction/token/EncryptedTokenStore.java | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 8e8bf37..07069c2 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -65,10 +65,8 @@ public static void main(String... args) throws Exception { var macKey = keyStore.getKey("hmac-key", keyPassword); var encKey = keyStore.getKey("aes-key", keyPassword); -// TokenStore tokenStore = new DatabaseTokenStore(database); TokenStore tokenStore = new JsonTokenStore(); tokenStore = new EncryptedTokenStore(tokenStore, encKey); - tokenStore = new HmacTokenStore(tokenStore, macKey); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java index eabd2f7..8d5131f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java @@ -61,8 +61,8 @@ public void revoke(Request request, String tokenId) { static byte[][] encrypt(Key key, byte[] message) { try { - var cipher = Cipher.getInstance("AES/CTR/NoPadding"); - var nonce = new byte[16]; + var cipher = Cipher.getInstance("ChaCha20-Poly1305"); + var nonce = new byte[12]; new SecureRandom().nextBytes(nonce); cipher.init(ENCRYPT_MODE, key, new IvParameterSpec(nonce)); var encrypted = cipher.doFinal(message); @@ -74,7 +74,7 @@ static byte[][] encrypt(Key key, byte[] message) { static byte[] decrypt(Key key, byte[] nonce, byte[] ciphertext) { try { - var cipher = Cipher.getInstance("AES/CTR/NoPadding"); + var cipher = Cipher.getInstance("ChaCha20-Poly1305"); cipher.init(DECRYPT_MODE, key, new IvParameterSpec(nonce)); return cipher.doFinal(ciphertext); } catch (GeneralSecurityException e) { From d2cba59a2df40a579c5797ea8b1b8564a1b54730 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 24 May 2019 21:04:29 +0100 Subject: [PATCH 079/209] Verify the audience as a JWT. --- .../manning/apisecurityinaction/token/JsonTokenStore.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java index b2ac74b..800de02 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java @@ -15,6 +15,7 @@ public String create(Request request, Token token) { var json = new JSONObject(); json.put("sub", token.username); json.put("exp", token.expiry.getEpochSecond()); + json.put("aud", List.of("https://localhost:4567")); json.put("attrs", token.attributes); var jsonBytes = json.toString().getBytes(UTF_8); @@ -29,8 +30,13 @@ public Optional read(Request request, String tokenId) { var json = new JSONObject(new String(decoded, UTF_8)); var expiry = Instant.ofEpochSecond(json.getInt("exp")); var username = json.getString("sub"); + var audience = json.getJSONArray("aud").toList(); var attrs = json.getJSONObject("attrs"); + if (!audience.contains("https://locahost:4567")) { + return Optional.empty(); + } + var token = new Token(expiry, username); for (var key : attrs.keySet()) { token.attributes.put(key, attrs.getString(key)); From 2d5d506534a432c3a1723c2ea694836e632ef07c Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 25 May 2019 08:57:19 +0100 Subject: [PATCH 080/209] Produce valid HS256 JWTs --- .../com/manning/apisecurityinaction/Main.java | 3 +- .../token/HmacTokenStore.java | 50 ++++++++++++++++--- .../token/JsonTokenStore.java | 2 +- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 07069c2..4c6ef9d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -11,6 +11,7 @@ import spark.embeddedserver.EmbeddedServers; import spark.embeddedserver.jetty.EmbeddedJettyFactory; +import javax.crypto.SecretKey; import java.io.FileInputStream; import java.nio.file.*; import java.security.KeyStore; @@ -66,7 +67,7 @@ public static void main(String... args) throws Exception { var encKey = keyStore.getKey("aes-key", keyPassword); TokenStore tokenStore = new JsonTokenStore(); - tokenStore = new EncryptedTokenStore(tokenStore, encKey); + tokenStore = new HmacTokenStore(tokenStore, macKey); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java index a410392..909d261 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java @@ -1,12 +1,14 @@ package com.manning.apisecurityinaction.token; +import org.json.JSONObject; import spark.Request; import javax.crypto.Mac; -import java.nio.charset.StandardCharsets; import java.security.*; import java.util.*; +import static java.nio.charset.StandardCharsets.UTF_8; + public class HmacTokenStore implements TokenStore { private final TokenStore delegate; @@ -20,6 +22,12 @@ public HmacTokenStore(TokenStore delegate, Key macKey) { @Override public String create(Request request, Token token) { var tokenId = delegate.create(request, token); + var header = new JSONObject() + .put("alg", jwsAlgorithm(macKey)) + .put("typ", "JWT").toString(); + header = Base64.getUrlEncoder().withoutPadding() + .encodeToString(header.getBytes(UTF_8)); + tokenId = header + '.' + tokenId; var tag = hmac(tokenId); return tokenId + '.' + @@ -32,7 +40,7 @@ private byte[] hmac(String tokenId) { var mac = Mac.getInstance(macKey.getAlgorithm()); mac.init(macKey); return mac.doFinal( - tokenId.getBytes(StandardCharsets.UTF_8)); + tokenId.getBytes(UTF_8)); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } @@ -40,15 +48,24 @@ private byte[] hmac(String tokenId) { @Override public Optional read(Request request, String tokenId) { - var index = tokenId.lastIndexOf('.'); - if (index == -1) { + var headerIndex = tokenId.indexOf('.'); + var tagIndex = tokenId.lastIndexOf('.'); + + var decoder = Base64.getUrlDecoder(); + var decodedHeader = decoder.decode( + tokenId.substring(0, headerIndex)); + var header = new JSONObject(new String(decodedHeader, UTF_8)); + + if (!"JWT".equals(header.getString("typ"))) { + return Optional.empty(); + } + if (!jwsAlgorithm(macKey).equals(header.getString("alg"))) { return Optional.empty(); } - var realTokenId = tokenId.substring(0, index); - var provided = Base64.getUrlDecoder() - .decode(tokenId.substring(index + 1)); - var computed = hmac(realTokenId); + var realTokenId = tokenId.substring(headerIndex + 1, tagIndex); + var provided = decoder.decode(tokenId.substring(tagIndex + 1)); + var computed = hmac(tokenId.substring(0, tagIndex)); if (!MessageDigest.isEqual(provided, computed)) { return Optional.empty(); @@ -57,6 +74,23 @@ public Optional read(Request request, String tokenId) { return delegate.read(request, realTokenId); } + private static String jwsAlgorithm(Key key) { + switch (key.getAlgorithm()) { + case "HmacSHA256": + case "1.2.840.113549.2.9": + return "HS256"; + case "HmacSHA384": + case "1.2.840.113549.2.10": + return "HS384"; + case "HmacSHA512": + case "1.2.840.113549.2.11": + return "HS512"; + default: + throw new IllegalStateException( + "unknown algorithm: " + key.getAlgorithm()); + } + } + @Override public void revoke(Request request, String tokenId) { var index = tokenId.lastIndexOf('.'); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java index 800de02..41382dd 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java @@ -33,7 +33,7 @@ public Optional read(Request request, String tokenId) { var audience = json.getJSONArray("aud").toList(); var attrs = json.getJSONObject("attrs"); - if (!audience.contains("https://locahost:4567")) { + if (!audience.contains("https://localhost:4567")) { return Optional.empty(); } From f65b0a7ad1531a3d2dc01c19f65dbb68cf0c7715 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 25 May 2019 08:58:29 +0100 Subject: [PATCH 081/209] Produce encrypted JWTs with nimbus-jose --- natter-api/pom.xml | 5 ++ .../com/manning/apisecurityinaction/Main.java | 3 +- .../token/JwtTokenStore.java | 63 +++++++++++++++++++ 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java diff --git a/natter-api/pom.xml b/natter-api/pom.xml index cda8f7b..84895e0 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -48,5 +48,10 @@ scrypt 1.4.0 + + com.nimbusds + nimbus-jose-jwt + 7.2.1 + \ No newline at end of file diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 4c6ef9d..e4df7f6 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -66,8 +66,7 @@ public static void main(String... args) throws Exception { var macKey = keyStore.getKey("hmac-key", keyPassword); var encKey = keyStore.getKey("aes-key", keyPassword); - TokenStore tokenStore = new JsonTokenStore(); - tokenStore = new HmacTokenStore(tokenStore, macKey); + TokenStore tokenStore = new JwtTokenStore((SecretKey) encKey); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java new file mode 100644 index 0000000..6665381 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java @@ -0,0 +1,63 @@ +package com.manning.apisecurityinaction.token; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.crypto.*; +import com.nimbusds.jwt.*; +import spark.Request; + +import javax.crypto.SecretKey; +import java.sql.Date; +import java.text.ParseException; +import java.util.Optional; + +public class JwtTokenStore implements TokenStore { + + private final SecretKey encKey; + + public JwtTokenStore(SecretKey encKey) { + this.encKey = encKey; + } + + @Override + public String create(Request request, Token token) { + var claimsBuilder = new JWTClaimsSet.Builder() + .subject(token.username) + .expirationTime(Date.from(token.expiry)); + token.attributes.forEach(claimsBuilder::claim); + + var header = new JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM); + var jwt = new EncryptedJWT(header, claimsBuilder.build()); + + try { + var encryptor = new DirectEncrypter(encKey); + jwt.encrypt(encryptor); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + + return jwt.serialize(); + } + + @Override + public Optional read(Request request, String tokenId) { + try { + var jwt = EncryptedJWT.parse(tokenId); + var decryptor = new DirectDecrypter(encKey); + + jwt.decrypt(decryptor); + + var claims = jwt.getJWTClaimsSet(); + var expiry = claims.getExpirationTime().toInstant(); + var subject = claims.getSubject(); + var token = new Token(expiry, subject); + for (var attr : claims.getClaims().keySet()) { + if ("exp".equals(attr) || "sub".equals(attr)) continue; + token.attributes.put(attr, claims.getStringClaim(attr)); + } + + return Optional.of(token); + } catch (ParseException | JOSEException e) { + return Optional.empty(); + } + } +} From 33d28393212433cb0dc94382b88f156d7ec01405 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 2 Jun 2019 17:39:46 +0100 Subject: [PATCH 082/209] JWT token revocation --- .../com/manning/apisecurityinaction/token/JwtTokenStore.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java index 6665381..8b26f06 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java @@ -60,4 +60,9 @@ public Optional read(Request request, String tokenId) { return Optional.empty(); } } + + @Override + public void revoke(Request request, String tokenId) { + // TODO + } } From 2b36be3045fbec116cb022a808165cb1d9c7ca2f Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 25 May 2019 16:32:45 +0100 Subject: [PATCH 083/209] Tidy up hand-rolled JWT validation code. --- .../com/manning/apisecurityinaction/Main.java | 3 +- .../token/HmacTokenStore.java | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index e4df7f6..4c6ef9d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -66,7 +66,8 @@ public static void main(String... args) throws Exception { var macKey = keyStore.getKey("hmac-key", keyPassword); var encKey = keyStore.getKey("aes-key", keyPassword); - TokenStore tokenStore = new JwtTokenStore((SecretKey) encKey); + TokenStore tokenStore = new JsonTokenStore(); + tokenStore = new HmacTokenStore(tokenStore, macKey); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java index 909d261..2b31b8d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java @@ -48,30 +48,31 @@ private byte[] hmac(String tokenId) { @Override public Optional read(Request request, String tokenId) { - var headerIndex = tokenId.indexOf('.'); - var tagIndex = tokenId.lastIndexOf('.'); + var parts = tokenId.split("\\."); + if (parts.length != 3) return Optional.empty(); + + var header = parts[0]; + var payload = parts[1]; + var tag = parts[2]; var decoder = Base64.getUrlDecoder(); - var decodedHeader = decoder.decode( - tokenId.substring(0, headerIndex)); - var header = new JSONObject(new String(decodedHeader, UTF_8)); + var provided = decoder.decode(tag); + var computed = hmac(header + '.' + payload); - if (!"JWT".equals(header.getString("typ"))) { + if (!MessageDigest.isEqual(provided, computed)) { return Optional.empty(); } - if (!jwsAlgorithm(macKey).equals(header.getString("alg"))) { + + var jwtHeader = new JSONObject( + new String(decoder.decode(header), UTF_8)); + if (!"JWT".equals(jwtHeader.getString("typ"))) { return Optional.empty(); } - - var realTokenId = tokenId.substring(headerIndex + 1, tagIndex); - var provided = decoder.decode(tokenId.substring(tagIndex + 1)); - var computed = hmac(tokenId.substring(0, tagIndex)); - - if (!MessageDigest.isEqual(provided, computed)) { + if (!jwsAlgorithm(macKey).equals(jwtHeader.getString("alg"))) { return Optional.empty(); } - return delegate.read(request, realTokenId); + return delegate.read(request, payload); } private static String jwsAlgorithm(Key key) { From 6e33227e1d6b69f23e0afbe390e82156f3b4b1e2 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 25 May 2019 17:34:40 +0100 Subject: [PATCH 084/209] Correct Date import --- .../com/manning/apisecurityinaction/token/JwtTokenStore.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java index 8b26f06..9fd6f1c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java @@ -6,9 +6,8 @@ import spark.Request; import javax.crypto.SecretKey; -import java.sql.Date; import java.text.ParseException; -import java.util.Optional; +import java.util.*; public class JwtTokenStore implements TokenStore { From b4ecafbdb1a8cb92445ceed083c1918e429fcb0a Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 25 May 2019 18:26:51 +0100 Subject: [PATCH 085/209] Fix JWT processing --- .../main/java/com/manning/apisecurityinaction/Main.java | 3 +-- .../manning/apisecurityinaction/token/JwtTokenStore.java | 9 +++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 4c6ef9d..e4df7f6 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -66,8 +66,7 @@ public static void main(String... args) throws Exception { var macKey = keyStore.getKey("hmac-key", keyPassword); var encKey = keyStore.getKey("aes-key", keyPassword); - TokenStore tokenStore = new JsonTokenStore(); - tokenStore = new HmacTokenStore(tokenStore, macKey); + TokenStore tokenStore = new JwtTokenStore((SecretKey) encKey); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java index 9fd6f1c..8899da5 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java @@ -21,6 +21,7 @@ public JwtTokenStore(SecretKey encKey) { public String create(Request request, Token token) { var claimsBuilder = new JWTClaimsSet.Builder() .subject(token.username) + .audience("https://localhost:4567") .expirationTime(Date.from(token.expiry)); token.attributes.forEach(claimsBuilder::claim); @@ -41,16 +42,20 @@ public String create(Request request, Token token) { public Optional read(Request request, String tokenId) { try { var jwt = EncryptedJWT.parse(tokenId); - var decryptor = new DirectDecrypter(encKey); + var decryptor = new DirectDecrypter(encKey); jwt.decrypt(decryptor); var claims = jwt.getJWTClaimsSet(); + if (!claims.getAudience().contains("https://localhost:4567")) { + return Optional.empty(); + } var expiry = claims.getExpirationTime().toInstant(); var subject = claims.getSubject(); var token = new Token(expiry, subject); + var ignore = Set.of("exp", "sub", "aud"); for (var attr : claims.getClaims().keySet()) { - if ("exp".equals(attr) || "sub".equals(attr)) continue; + if (ignore.contains(attr)) continue; token.attributes.put(attr, claims.getStringClaim(attr)); } From b3e7df964905cdf84086640fd180dc8bf5b2ec73 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 25 May 2019 21:07:32 +0100 Subject: [PATCH 086/209] Extract JWT header into a separate wrapper store --- natter-api/keystore.p12 | Bin 543 -> 755 bytes .../com/manning/apisecurityinaction/Main.java | 8 ++- .../token/HmacTokenStore.java | 49 +++-------------- .../token/JwtHeaderTokenStore.java | 50 ++++++++++++++++++ 4 files changed, 64 insertions(+), 43 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java diff --git a/natter-api/keystore.p12 b/natter-api/keystore.p12 index b48062c887d9cf14e2cc39c30400e2ccbbe4c58b..9007e6c11b68583815fceb513feab72452eed153 100644 GIT binary patch delta 281 zcmbQw@|o4vpo!@{6C*Q_TEWJt)#lOmotKfFaX}N)OqM35sX*b0K-`NeRR@%+0t%OH zv|Y@|)Oc~CgIRsMkd57m70W!k{mXaUW4FAvb%)MR+nyd0JJUna@7GH1jS%ft`V(0Vf-)HXk#S z6e|OZ$oksHJ=uw#KMG2;G|wrV*4yw=vWZ2cu9K@|x#rtvFQR^)+$gIm`)h7+785gL G+X4VHCt{%h delta 134 zcmey&I-kYXpovMEiILf$iSY^>r&gOs+jm|@cE$xwj7M3T7!Lx4_X6=wRH;=!sbxUn zMH_7wGuqo3C>n6Gv1;=%GfA;Bu!y9b@K_*vLG|BRp6&dPH4?aY#8&mPi0u0AZ~atg YszW+M)!$-)D#!2la!r_+8QT^B0D5aH2LJ#7 diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index e4df7f6..a4d2581 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -66,7 +66,13 @@ public static void main(String... args) throws Exception { var macKey = keyStore.getKey("hmac-key", keyPassword); var encKey = keyStore.getKey("aes-key", keyPassword); - TokenStore tokenStore = new JwtTokenStore((SecretKey) encKey); + var header = new JSONObject() + .put("alg", "HS256") + .put("typ", "JWT"); + + TokenStore tokenStore = new JsonTokenStore(); + tokenStore = new JwtHeaderTokenStore(tokenStore, header); + tokenStore = new HmacTokenStore(tokenStore, macKey); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java index 2b31b8d..3ae613e 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java @@ -1,6 +1,5 @@ package com.manning.apisecurityinaction.token; -import org.json.JSONObject; import spark.Request; import javax.crypto.Mac; @@ -22,12 +21,6 @@ public HmacTokenStore(TokenStore delegate, Key macKey) { @Override public String create(Request request, Token token) { var tokenId = delegate.create(request, token); - var header = new JSONObject() - .put("alg", jwsAlgorithm(macKey)) - .put("typ", "JWT").toString(); - header = Base64.getUrlEncoder().withoutPadding() - .encodeToString(header.getBytes(UTF_8)); - tokenId = header + '.' + tokenId; var tag = hmac(tokenId); return tokenId + '.' + @@ -39,8 +32,7 @@ private byte[] hmac(String tokenId) { try { var mac = Mac.getInstance(macKey.getAlgorithm()); mac.init(macKey); - return mac.doFinal( - tokenId.getBytes(UTF_8)); + return mac.doFinal(tokenId.getBytes(UTF_8)); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } @@ -48,48 +40,21 @@ private byte[] hmac(String tokenId) { @Override public Optional read(Request request, String tokenId) { - var parts = tokenId.split("\\."); - if (parts.length != 3) return Optional.empty(); + var index = tokenId.lastIndexOf('.'); + if (index == -1) return Optional.empty(); - var header = parts[0]; - var payload = parts[1]; - var tag = parts[2]; + var realTokenId = tokenId.substring(0, index); + var tag = tokenId.substring(index + 1); var decoder = Base64.getUrlDecoder(); var provided = decoder.decode(tag); - var computed = hmac(header + '.' + payload); + var computed = hmac(realTokenId); if (!MessageDigest.isEqual(provided, computed)) { return Optional.empty(); } - var jwtHeader = new JSONObject( - new String(decoder.decode(header), UTF_8)); - if (!"JWT".equals(jwtHeader.getString("typ"))) { - return Optional.empty(); - } - if (!jwsAlgorithm(macKey).equals(jwtHeader.getString("alg"))) { - return Optional.empty(); - } - - return delegate.read(request, payload); - } - - private static String jwsAlgorithm(Key key) { - switch (key.getAlgorithm()) { - case "HmacSHA256": - case "1.2.840.113549.2.9": - return "HS256"; - case "HmacSHA384": - case "1.2.840.113549.2.10": - return "HS384"; - case "HmacSHA512": - case "1.2.840.113549.2.11": - return "HS512"; - default: - throw new IllegalStateException( - "unknown algorithm: " + key.getAlgorithm()); - } + return delegate.read(request, realTokenId); } @Override diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java new file mode 100644 index 0000000..ff8672e --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java @@ -0,0 +1,50 @@ +package com.manning.apisecurityinaction.token; + +import org.json.JSONObject; +import spark.Request; + +import java.util.*; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class JwtHeaderTokenStore implements TokenStore { + + private final TokenStore delegate; + private final JSONObject header; + + public JwtHeaderTokenStore(TokenStore delegate, JSONObject header) { + this.delegate = delegate; + this.header = header; + } + + @Override + public String create(Request request, Token token) { + var tokenId = delegate.create(request, token); + var headerBytes = header.toString().getBytes(UTF_8); + return Base64.getUrlEncoder().withoutPadding() + .encodeToString(headerBytes) + '.' + tokenId; + } + + @Override + public Optional read(Request request, String tokenId) { + var index = tokenId.indexOf('.'); + if (index == -1) return Optional.empty(); + + var encodedHeader = tokenId.substring(0, index); + var realTokenId = tokenId.substring(index + 1); + + var decodedHeader = Base64.getUrlDecoder() + .decode(encodedHeader); + var suppliedHeader = new JSONObject( + new String(decodedHeader, UTF_8)); + + for (var expected : this.header.keySet()) { + if (!Objects.equals(this.header.get(expected), + suppliedHeader.get(expected))) { + return Optional.empty(); + } + } + + return delegate.read(request, realTokenId); + } +} From 5ce08b7b4b723d177e57755c13d3f0458aa577c9 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 2 Jun 2019 17:41:45 +0100 Subject: [PATCH 087/209] JwtHeaderTokenStore revocation --- .../apisecurityinaction/token/JwtHeaderTokenStore.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java index ff8672e..634bab0 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java @@ -47,4 +47,13 @@ public Optional read(Request request, String tokenId) { return delegate.read(request, realTokenId); } + + @Override + public void revoke(Request request, String tokenId) { + var index = tokenId.indexOf('.'); + if (index == -1) return; + + var realTokenId = tokenId.substring(index + 1); + delegate.revoke(request, realTokenId); + } } From 1dd75531cfc994d7ba381f2451c8cd7de7963ecc Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 25 May 2019 21:28:43 +0100 Subject: [PATCH 088/209] Switch back to the JwtTokenStore --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index a4d2581..3aa30da 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -70,9 +70,7 @@ public static void main(String... args) throws Exception { .put("alg", "HS256") .put("typ", "JWT"); - TokenStore tokenStore = new JsonTokenStore(); - tokenStore = new JwtHeaderTokenStore(tokenStore, header); - tokenStore = new HmacTokenStore(tokenStore, macKey); + TokenStore tokenStore = new JwtTokenStore((SecretKey) encKey); var tokenController = new TokenController(tokenStore); before(userController::authenticate); From 6f1e3e75f2aae1d12033bbd43779709713f32134 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 27 May 2019 11:38:09 +0100 Subject: [PATCH 089/209] Add types to enforce security properties --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 4 +++- .../apisecurityinaction/controller/TokenController.java | 6 +++--- .../apisecurityinaction/token/AuthenticatedTokenStore.java | 4 ++++ .../apisecurityinaction/token/ConfidentialTokenStore.java | 4 ++++ .../manning/apisecurityinaction/token/CookieTokenStore.java | 2 +- .../apisecurityinaction/token/DatabaseTokenStore.java | 2 +- .../apisecurityinaction/token/EncryptedTokenStore.java | 2 +- .../manning/apisecurityinaction/token/HmacTokenStore.java | 2 +- .../manning/apisecurityinaction/token/JwtTokenStore.java | 2 +- .../manning/apisecurityinaction/token/SecureTokenStore.java | 5 +++++ 10 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/AuthenticatedTokenStore.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/ConfidentialTokenStore.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/SecureTokenStore.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 3aa30da..c1a371c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -70,7 +70,9 @@ public static void main(String... args) throws Exception { .put("alg", "HS256") .put("typ", "JWT"); - TokenStore tokenStore = new JwtTokenStore((SecretKey) encKey); + SecureTokenStore tokenStore = + new EncryptedTokenStore(new JsonTokenStore(), encKey); + var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java index 7417b7f..9b5b78f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java @@ -1,6 +1,6 @@ package com.manning.apisecurityinaction.controller; -import com.manning.apisecurityinaction.token.TokenStore; +import com.manning.apisecurityinaction.token.*; import org.json.JSONObject; import spark.*; @@ -11,9 +11,9 @@ public class TokenController { - private final TokenStore tokenStore; + private final SecureTokenStore tokenStore; - public TokenController(TokenStore tokenStore) { + public TokenController(SecureTokenStore tokenStore) { this.tokenStore = tokenStore; } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/AuthenticatedTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/AuthenticatedTokenStore.java new file mode 100644 index 0000000..b55f0d0 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/AuthenticatedTokenStore.java @@ -0,0 +1,4 @@ +package com.manning.apisecurityinaction.token; + +public interface AuthenticatedTokenStore extends TokenStore { +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/ConfidentialTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/ConfidentialTokenStore.java new file mode 100644 index 0000000..4de3c70 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/ConfidentialTokenStore.java @@ -0,0 +1,4 @@ +package com.manning.apisecurityinaction.token; + +public interface ConfidentialTokenStore extends TokenStore { +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java index e82bb4e..ad0d265 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java @@ -6,7 +6,7 @@ import spark.Request; -public class CookieTokenStore implements TokenStore { +public class CookieTokenStore implements SecureTokenStore { @Override public String create(Request request, Token token) { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java index 2e446a3..ba7bd90 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java @@ -10,7 +10,7 @@ import java.util.*; import java.util.concurrent.*; -public class DatabaseTokenStore implements TokenStore { +public class DatabaseTokenStore implements SecureTokenStore { private static final Logger logger = LoggerFactory.getLogger(DatabaseTokenStore.class); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java index 8d5131f..354097b 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java @@ -9,7 +9,7 @@ import static javax.crypto.Cipher.*; -public class EncryptedTokenStore implements TokenStore { +public class EncryptedTokenStore implements SecureTokenStore { private final TokenStore delegate; private final Key encryptionKey; diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java index 3ae613e..c34c7d0 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java @@ -8,7 +8,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; -public class HmacTokenStore implements TokenStore { +public class HmacTokenStore implements SecureTokenStore { private final TokenStore delegate; private final Key macKey; diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java index 8899da5..33a9941 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java @@ -9,7 +9,7 @@ import java.text.ParseException; import java.util.*; -public class JwtTokenStore implements TokenStore { +public class JwtTokenStore implements SecureTokenStore { private final SecretKey encKey; diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SecureTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SecureTokenStore.java new file mode 100644 index 0000000..c4d6736 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SecureTokenStore.java @@ -0,0 +1,5 @@ +package com.manning.apisecurityinaction.token; + +public interface SecureTokenStore extends ConfidentialTokenStore, + AuthenticatedTokenStore { +} From b40cde361d435c7a9e51ec1bdc02ea9cc4163fbd Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 23 May 2019 12:32:13 +0100 Subject: [PATCH 090/209] Add the HmacTokenStore --- natter-api/keystore.p12 | Bin 0 -> 327 bytes .../com/manning/apisecurityinaction/Main.java | 10 +++ .../token/DatabaseTokenStore.java | 3 +- .../token/HmacTokenStore.java | 76 ++++++++++++++++++ natter-api/src/main/resources/schema.sql | 5 +- 5 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 natter-api/keystore.p12 create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java diff --git a/natter-api/keystore.p12 b/natter-api/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..45dc68cc56d0bf3674d8d73fcf515c230e82be94 GIT binary patch literal 327 zcmXqLVsvI=WHxC0%f_kI=F#?@myw-uLF0Rt#y19yFAW->p$Ok#X}oICc*&sgJR3Ju zH4hgf>w+qS3Sb z84d@qSlI1&wEi0hYf$+5K2xTSln)>Kw4C!=AN+WHUgGfF@WXrN7hl`U6JfX4YDe0h zM{r zZmNQzsi}pTp^2e^rJ0$fft`V(0Vf-)HXk#S6e|OZNZCccPnYJsEct%A|ASnyTxXX^ iXgQ0>B$?19`y+k(>)&maeKvKu#{=Wy<4nwqZ3_U read(Request request, String tokenId) { + var index = tokenId.lastIndexOf('.'); + if (index == -1) { + return Optional.empty(); + } + var realTokenId = tokenId.substring(0, index); + + var provided = Base64.getUrlDecoder() + .decode(tokenId.substring(index + 1)); + var computed = hmac(realTokenId); + + if (!MessageDigest.isEqual(provided, computed)) { + return Optional.empty(); + } + + return delegate.read(request, realTokenId); + } + + @Override + public void revoke(Request request, String tokenId) { + var index = tokenId.lastIndexOf('.'); + if (index == -1) return; + var realTokenId = tokenId.substring(0, index); + + var provided = Base64.getUrlDecoder() + .decode(tokenId.substring(index + 1)); + var computed = hmac(realTokenId); + + if (!MessageDigest.isEqual(provided, computed)) { + return; + } + + delegate.revoke(request, realTokenId); + } +} diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 7533c95..74d3e98 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -50,4 +50,7 @@ GRANT DELETE ON messages TO natter_api_user; GRANT SELECT, INSERT ON users TO natter_api_user; GRANT SELECT, INSERT ON audit_log TO natter_api_user; GRANT SELECT, INSERT ON permissions TO natter_api_user; -GRANT SELECT, INSERT, DELETE ON tokens TO natter_api_user; \ No newline at end of file +GRANT SELECT, INSERT ON tokens TO natter_api_user; + +CREATE USER token_mgmt_user PASSWORD 'password'; +GRANT SELECT, DELETE ON tokens TO token_mgmt_user; \ No newline at end of file From 1fe4f46de7410abc60c272d1c993c87144f5848c Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 4 Jun 2019 12:34:48 +0100 Subject: [PATCH 091/209] Revert change to database permissions --- natter-api/src/main/resources/schema.sql | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 74d3e98..dfad384 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -50,7 +50,4 @@ GRANT DELETE ON messages TO natter_api_user; GRANT SELECT, INSERT ON users TO natter_api_user; GRANT SELECT, INSERT ON audit_log TO natter_api_user; GRANT SELECT, INSERT ON permissions TO natter_api_user; -GRANT SELECT, INSERT ON tokens TO natter_api_user; - -CREATE USER token_mgmt_user PASSWORD 'password'; -GRANT SELECT, DELETE ON tokens TO token_mgmt_user; \ No newline at end of file +GRANT SELECT, INSERT, DELETE ON tokens TO natter_api_user; From 728125d9cda706792460cd8c5bbc0a9938e5f0e0 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 4 Jun 2019 12:36:26 +0100 Subject: [PATCH 092/209] Switch back to using the JwtTokenStore --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index c1a371c..ea0a846 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -70,8 +70,9 @@ public static void main(String... args) throws Exception { .put("alg", "HS256") .put("typ", "JWT"); + var tokenWhitelist = new DatabaseTokenStore(database); SecureTokenStore tokenStore = - new EncryptedTokenStore(new JsonTokenStore(), encKey); + new JwtTokenStore((SecretKey) encKey, tokenWhitelist); var tokenController = new TokenController(tokenStore); From de2d8f839534822e98da214179cc627b8f3d94de Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 4 Jun 2019 12:36:03 +0100 Subject: [PATCH 093/209] Implement token whitelisting for JWTs --- .../token/JwtTokenStore.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java index 33a9941..582b21b 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java @@ -12,14 +12,21 @@ public class JwtTokenStore implements SecureTokenStore { private final SecretKey encKey; + private final DatabaseTokenStore tokenWhitelist; - public JwtTokenStore(SecretKey encKey) { + public JwtTokenStore(SecretKey encKey, + DatabaseTokenStore tokenWhitelist) { this.encKey = encKey; + this.tokenWhitelist = tokenWhitelist; } @Override public String create(Request request, Token token) { + var whitelistToken = new Token(token.expiry, token.username); + var jwtId = tokenWhitelist.create(request, whitelistToken); + var claimsBuilder = new JWTClaimsSet.Builder() + .jwtID(jwtId) .subject(token.username) .audience("https://localhost:4567") .expirationTime(Date.from(token.expiry)); @@ -47,11 +54,17 @@ public Optional read(Request request, String tokenId) { jwt.decrypt(decryptor); var claims = jwt.getJWTClaimsSet(); + var jwtId = claims.getJWTID(); + if (tokenWhitelist.read(request, jwtId).isEmpty()) { + return Optional.empty(); + } + if (!claims.getAudience().contains("https://localhost:4567")) { return Optional.empty(); } var expiry = claims.getExpirationTime().toInstant(); var subject = claims.getSubject(); + var token = new Token(expiry, subject); var ignore = Set.of("exp", "sub", "aud"); for (var attr : claims.getClaims().keySet()) { @@ -67,6 +80,16 @@ public Optional read(Request request, String tokenId) { @Override public void revoke(Request request, String tokenId) { - // TODO + try { + var jwt = EncryptedJWT.parse(tokenId); + + var decryptor = new DirectDecrypter(encKey); + jwt.decrypt(decryptor); + var claims = jwt.getJWTClaimsSet(); + + tokenWhitelist.revoke(request, claims.getJWTID()); + } catch (ParseException | JOSEException e) { + throw new IllegalArgumentException("invalid token", e); + } } } From a26536db6154e0bf905078f549dd3c99e9cad7ce Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 27 May 2019 17:17:37 +0100 Subject: [PATCH 094/209] Update README with links to chapter branches --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 843d258..cc74972 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,25 @@ The branches named "chapter02-end", "chapter03-end" etc give the final source code after all the alterations in that chapter. Typically the source code at the end of a chapter is also identical to the start of the next chapter. + +## Chapters + +### Chapter 2 - Secure API development + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter02) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter02-end) + +### Chapter 3 - Securing the Natter API + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter03) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter03-end) + +### Chapter 4 - Session cookie authentication + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter04) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter04-end) + +### Chapter 5 - Modern token-based authentication + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter05) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter05-end) From e1a2dada168dfacd05e4abe00fad983efb0cc274 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 31 May 2019 21:20:08 +0100 Subject: [PATCH 095/209] Add support for scoped tokens. --- .../com/manning/apisecurityinaction/Main.java | 12 ++++++++++ .../controller/TokenController.java | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index ea0a846..d95c024 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -92,21 +92,31 @@ public static void main(String... args) throws Exception { post("/users", userController::registerUser); before("/spaces", userController::requireAuthentication); + before("/spaces", + tokenController.requireScope("POST", "create_space")); post("/spaces", spaceController::createSpace); + before("/spaces/*/messages", + tokenController.requireScope("POST", "post_message")); before("/spaces/:spaceId/messages", userController.requirePermission("POST", "w")); post("/spaces/:spaceId/messages", spaceController::postMessage); + before("/spaces/*/messages/*", + tokenController.requireScope("GET", "read_message")); before("/spaces/:spaceId/messages/*", userController.requirePermission("GET", "r")); get("/spaces/:spaceId/messages/:msgId", spaceController::readMessage); + before("/spaces/*/messages", + tokenController.requireScope("GET", "list_messages")); before("/spaces/:spaceId/messages", userController.requirePermission("GET", "r")); get("/spaces/:spaceId/messages", spaceController::findMessages); + before("/spaces/*/members", + tokenController.requireScope("POST", "add_member")); before("/spaces/:spaceId/members", userController.requirePermission("POST", "rwd")); post("/spaces/:spaceId/members", spaceController::addMember); @@ -114,6 +124,8 @@ public static void main(String... args) throws Exception { var moderatorController = new ModeratorController(database); + before("/spaces/*/messages/*", + tokenController.requireScope("DELETE", "delete_message")); before("/spaces/:spaceId/messages/*", userController.requirePermission("DELETE", "d")); delete("/spaces/:spaceId/messages/:msgId", diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java index 9b5b78f..841b0e7 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java @@ -6,6 +6,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Arrays; import static spark.Spark.halt; @@ -22,6 +23,12 @@ public JSONObject login(Request request, Response response) { var expiry = Instant.now().plus(10, ChronoUnit.MINUTES); var token = new TokenStore.Token(expiry, subject); + + var scope = request.queryParams("scope"); + if (scope != null) { + token.attributes.put("scope", scope); + } + var tokenId = tokenStore.create(request, token); response.status(201); @@ -49,6 +56,22 @@ public void validateToken(Request request, Response response) { }); } + public Filter requireScope(String method, String requiredScope) { + return (request, response) -> { + if (!method.equals(request.requestMethod())) return; + + var tokenScope = request.attribute("scope"); + if (tokenScope == null) return; + if (!Arrays.asList(tokenScope.split(" ")) + .contains(requiredScope)) { + response.header("WWW-Authenticate", + "Bearer error=\"insufficient_scope\"," + + "scope=\"" + requiredScope + "\""); + halt(403); + } + }; + } + public JSONObject logout(Request request, Response response) { var tokenId = request.headers("Authorization"); if (tokenId == null || !tokenId.startsWith("Bearer ")) { From eab572605b9f2c25e62b6d8711477af9ec0bf6e0 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 2 Jun 2019 08:48:08 +0100 Subject: [PATCH 096/209] OAuth2TokenStore --- .../com/manning/apisecurityinaction/Main.java | 1 + .../token/OAuth2TokenStore.java | 89 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index d95c024..baa8892 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -13,6 +13,7 @@ import javax.crypto.SecretKey; import java.io.FileInputStream; +import java.net.URI; import java.nio.file.*; import java.security.KeyStore; import java.sql.Connection; diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java new file mode 100644 index 0000000..a14c9b0 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java @@ -0,0 +1,89 @@ +package com.manning.apisecurityinaction.token; + +import org.json.JSONObject; +import spark.Request; + +import java.io.IOException; +import java.net.*; +import java.net.http.*; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class OAuth2TokenStore implements SecureTokenStore { + + private final URI introspectionEndpoint; + private final String authorization; + + private final HttpClient httpClient; + + public OAuth2TokenStore(URI introspectionEndpoint, + String clientId, String clientSecret) { + this.introspectionEndpoint = introspectionEndpoint; + + var credentials = URLEncoder.encode(clientId, UTF_8) + ":" + + URLEncoder.encode(clientSecret, UTF_8); + this.authorization = "Basic " + Base64.getEncoder() + .encodeToString(credentials.getBytes(UTF_8)); + + this.httpClient = HttpClient.newHttpClient(); + } + + @Override + public String create(Request request, Token token) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional read(Request request, String tokenId) { + + var form = "token=" + URLEncoder.encode(tokenId, UTF_8); + + var httpRequest = HttpRequest.newBuilder() + .uri(introspectionEndpoint) + .header("Authorization", authorization) + .POST(BodyPublishers.ofString(form)) + .build(); + + try { + var httpResponse = httpClient.send(httpRequest, + BodyHandlers.ofString()); + + if (httpResponse.statusCode() == 200) { + var json = new JSONObject(httpResponse.body()); + + if (json.getBoolean("active")) { + return processResponse(json); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + return Optional.empty(); + } + + private Optional processResponse(JSONObject response) { + var now = Instant.now(); + + var defaultExpiry = now.plus(10, ChronoUnit.SECONDS); + var expiry = Instant.ofEpochSecond( + response.optLong("exp", defaultExpiry.getEpochSecond())); + + var subject = response.getString("sub"); + var token = new Token(expiry, subject); + + for (var attr : response.keySet()) { + token.attributes.put(attr, response.optString(attr)); + } + + return Optional.of(token); + } +} From ee593910e100ee185ef2d4879bd25ba14383a9ba Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 2 Jun 2019 17:52:30 +0100 Subject: [PATCH 097/209] OAuth2 token revocation --- .../token/OAuth2TokenStore.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java index a14c9b0..64eabb3 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java @@ -17,13 +17,16 @@ public class OAuth2TokenStore implements SecureTokenStore { private final URI introspectionEndpoint; + private final URI revocationEndpoint; private final String authorization; private final HttpClient httpClient; public OAuth2TokenStore(URI introspectionEndpoint, + URI revocationEndpoint, String clientId, String clientSecret) { this.introspectionEndpoint = introspectionEndpoint; + this.revocationEndpoint = revocationEndpoint; var credentials = URLEncoder.encode(clientId, UTF_8) + ":" + URLEncoder.encode(clientSecret, UTF_8); @@ -70,6 +73,26 @@ public Optional read(Request request, String tokenId) { return Optional.empty(); } + @Override + public void revoke(Request request, String tokenId) { + var form = "token=" + URLEncoder.encode(tokenId, UTF_8); + + var httpRequest = HttpRequest.newBuilder() + .uri(revocationEndpoint) + .header("Authorization", authorization) + .POST(BodyPublishers.ofString(form)) + .build(); + + try { + httpClient.send(httpRequest, BodyHandlers.discarding()); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + private Optional processResponse(JSONObject response) { var now = Instant.now(); From 68c255019a3625a187d80c52499fa9b8867e11a8 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 2 Jun 2019 18:07:09 +0100 Subject: [PATCH 098/209] Updated README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index cc74972..15bdfe9 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,13 @@ of the next chapter. - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter05) - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter05-end) + +### Chapter 6 - Self-contained tokens and JWTs + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter06) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter06-end) + +### Chapter 7 - OAuth 2 and OpenID Connect + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter07) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter07-end) From d089512ffcb7f20bb9995cceff407e1148f8a335 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 1 Jul 2019 13:28:07 +0100 Subject: [PATCH 099/209] Improve OAuth2TokenStore --- natter-api/as.example.com.p12 | Bin 0 -> 1506 bytes .../com/manning/apisecurityinaction/Main.java | 16 +++- .../RevokeAccessToken.java | 49 +++++++++++ .../token/OAuth2TokenStore.java | 76 ++++++++++++++---- 4 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 natter-api/as.example.com.p12 create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java diff --git a/natter-api/as.example.com.p12 b/natter-api/as.example.com.p12 new file mode 100644 index 0000000000000000000000000000000000000000..0000acc2f8cdf455e6fd48dc768d4888a543177d GIT binary patch literal 1506 zcmV<81s(b@f(70J0Ru3C1(ya1Duzgg_YDCD0ic2fhy;QKgfM~yfG~mudM)9z`A6$bGR8r{9q>>NiXJ^-u!Ua}dd2zNx+_>Ej1 zvne`^YR7X#A}J$jNlb zb{Iyw{8Cw^6|?jsmEbbCTPZIR(b>Wm;oG%kMUrU{Ai@~5N^Py4hT2-S<}=sBwlJrC z#lBTVq@x)JzkUuzSr2qy%HOi$PY<9MNpoQvZabKYb+{hUk}@rjn0+Rdhjedk7F$pe z?5g1gKL_i;%B5h44J<4sZ=E{LMw1k#CiEwVQi`)5&rA_!X~UQXb#RdeYRlVsMv}{5 zp*`WW2#4zrOp`;=R}&6R13Lk{PfFnASRLNxnYrK5F?myBwnjHOv$x!l`ufG($Fm109k!+ zy?of)YxZjah(8Xwf|IPJa-cD<-p-GEa8A{HxNd^g2#50|Z5c_#RS_qE^(Xzr4()nS zE`Ay+m~~fa`bjP`!CYLsk6E9d`5NQuZbk&bJ=_R9IRxL11eaW{^`XJhvI>I*YznE^ zDLk~leh?xkc5;1I7>nkv9930$mnVxU@+ONM#`1=e|3Dj++UxZ<{&=}CT1_f(ctv#% z5xp^OS^o1(924=!YSD?F@z%D$(LQ&1ExU(Uz48UAQiJiR8OY@hjsqD`dPT-!_TM!VkG# z`|(~_dhbg?KHpg2cC;ZHWe?Dw396H}4V2!Lt%~}gu@|9ycJ?^(Vz{==aHncCZ}Rda zZQYTDoj+00GirUgnSa104qbJl=9ppwxz+6Lg}6oh`EUAc_j|G81U(&vB+#(>UnEw4faZ;#WyP-YE206}ZytAZ zAFgg`#=MNRqt!?HzaO?g`vM3=`a%MTO03TjQZwX)X#u}Q?xDtsw>T`VUKvNO2%jIl zUU%x8R~QvPEr(BThi~YHYk^OUQC9KHQq%U$jj;Oru-v8!7F8;V}+fAqXNqpKAaE z;qArDaM;ntqN5bKTC~hWq!Wk=kq^H;sK|WInhL#b5 zkI1!95NZ(Gf*dh67J4TOy#l+jiiPd4hC-u$kN%5;pdGSjdla%YtAm!knJ_*uAutIB z1uG5%0vZJX1Qe6u5LqxE_o+xLWR&ybXHOj7spSL|03+#jN~Dh7Qb*W&@lKmhHYW1D I0s{etp!0*qE&u=k literal 0 HcmV?d00001 diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index baa8892..fca12fa 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -11,7 +11,6 @@ import spark.embeddedserver.EmbeddedServers; import spark.embeddedserver.jetty.EmbeddedJettyFactory; -import javax.crypto.SecretKey; import java.io.FileInputStream; import java.net.URI; import java.nio.file.*; @@ -71,9 +70,16 @@ public static void main(String... args) throws Exception { .put("alg", "HS256") .put("typ", "JWT"); - var tokenWhitelist = new DatabaseTokenStore(database); - SecureTokenStore tokenStore = - new JwtTokenStore((SecretKey) encKey, tokenWhitelist); + var clientId = "test"; + var clientSecret = + System.getProperty("client_secret", "password"); + var introspectionEndpoint = + URI.create("https://as.example.com:8443/oauth2/introspect"); + var revocationEndpoint = + URI.create("https://as.example.com:8443/oauth2/token/revoke"); + SecureTokenStore tokenStore = new OAuth2TokenStore( + introspectionEndpoint, revocationEndpoint, + clientId, clientSecret); var tokenController = new TokenController(tokenStore); @@ -85,6 +91,8 @@ public static void main(String... args) throws Exception { afterAfter(auditController::auditRequestEnd); before("/sessions", userController::requireAuthentication); + before("/sessions", + tokenController.requireScope("POST", "create_token")); post("/sessions", tokenController::login); delete("/sessions", tokenController::logout); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java b/natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java new file mode 100644 index 0000000..0717a3b --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java @@ -0,0 +1,49 @@ +package com.manning.apisecurityinaction; + +import java.net.*; +import java.net.http.*; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Base64; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Sample indicating how to revoke an OAuth 2 access or refresh + * token. + */ +public class RevokeAccessToken { + + private static final URI revocationEndpoint = + URI.create("https://openam.example.com:8443/openam/oauth2/token/revoke"); + + public static void main(String...args) throws Exception { + + if (args.length != 3) { + throw new IllegalArgumentException( + "RevokeAccessToken clientId clientSecret token"); + } + + var clientId = args[0]; + var clientSecret = args[1]; + var token = args[2]; + + var credentials = URLEncoder.encode(clientId, UTF_8) + + ":" + URLEncoder.encode(clientSecret, UTF_8); + var authorization = "Basic " + Base64.getEncoder() + .encodeToString(credentials.getBytes(UTF_8)); + + var httpClient = HttpClient.newHttpClient(); + + var form = "token=" + URLEncoder.encode(token, UTF_8) + + "&token_type_hint=access_token"; + + var httpRequest = HttpRequest.newBuilder() + .uri(revocationEndpoint) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("Authorization", authorization) + .POST(HttpRequest.BodyPublishers.ofString(form)) + .build(); + + httpClient.send(httpRequest, BodyHandlers.discarding()); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java index 64eabb3..388c0ae 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java @@ -3,13 +3,14 @@ import org.json.JSONObject; import spark.Request; -import java.io.IOException; +import javax.net.ssl.*; +import java.io.*; import java.net.*; import java.net.http.*; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandlers; +import java.security.*; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.*; import static java.nio.charset.StandardCharsets.UTF_8; @@ -33,7 +34,45 @@ public OAuth2TokenStore(URI introspectionEndpoint, this.authorization = "Basic " + Base64.getEncoder() .encodeToString(credentials.getBytes(UTF_8)); - this.httpClient = HttpClient.newHttpClient(); + // Use "Intermediate" settings from https://wiki.mozilla.org/Security/Server_Side_TLS + // As of 2019-06-30 + var sslParams = new SSLParameters(); + sslParams.setProtocols(new String[] { "TLSv1.3", "TLSv1.2" }); + sslParams.setCipherSuites(new String[] { + // TLS 1.3 cipher suites + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + // TLS 1.2 cipher suites + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" + }); + sslParams.setUseCipherSuitesOrder(true); + sslParams.setEndpointIdentificationAlgorithm("HTTPS"); + + try { + var trustedCerts = KeyStore.getInstance("PKCS12"); + trustedCerts.load( + new FileInputStream("as.example.com.p12"), + "changeit".toCharArray()); + + var tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(trustedCerts); + var sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(null, tmf.getTrustManagers(), null); + + this.httpClient = HttpClient.newBuilder() + .sslParameters(sslParams) + .sslContext(sslContext) + .build(); + + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException(e); + } } @Override @@ -43,11 +82,16 @@ public String create(Request request, Token token) { @Override public Optional read(Request request, String tokenId) { + if (!tokenId.matches("[\\x20-\\x7E]{1,1024}")) { + return Optional.empty(); + } - var form = "token=" + URLEncoder.encode(tokenId, UTF_8); + var form = "token=" + URLEncoder.encode(tokenId, UTF_8) + + "&token_type_hint=access_token"; var httpRequest = HttpRequest.newBuilder() .uri(introspectionEndpoint) + .header("Content-Type", "application/x-www-form-urlencoded") .header("Authorization", authorization) .POST(BodyPublishers.ofString(form)) .build(); @@ -75,10 +119,16 @@ public Optional read(Request request, String tokenId) { @Override public void revoke(Request request, String tokenId) { - var form = "token=" + URLEncoder.encode(tokenId, UTF_8); + if (!tokenId.matches("[\\x20-\\x7E]{1,1024}")) { + throw new IllegalArgumentException("invalid token"); + } + + var form = "token=" + URLEncoder.encode(tokenId, UTF_8) + + "&token_type_hint=access_token"; var httpRequest = HttpRequest.newBuilder() .uri(revocationEndpoint) + .header("Content-Type", "application/x-www-form-urlencoded") .header("Authorization", authorization) .POST(BodyPublishers.ofString(form)) .build(); @@ -94,18 +144,16 @@ public void revoke(Request request, String tokenId) { } private Optional processResponse(JSONObject response) { - var now = Instant.now(); - - var defaultExpiry = now.plus(10, ChronoUnit.SECONDS); - var expiry = Instant.ofEpochSecond( - response.optLong("exp", defaultExpiry.getEpochSecond())); + var expiry = Instant.ofEpochSecond(response.optLong("exp")); + var subject = response.optString("sub"); + if (subject == null) { + return Optional.empty(); + } - var subject = response.getString("sub"); var token = new Token(expiry, subject); - for (var attr : response.keySet()) { - token.attributes.put(attr, response.optString(attr)); - } + token.attributes.put("scope", response.getString("scope")); + token.attributes.put("client_id", response.optString("client_id")); return Optional.of(token); } From 2b5f672a112fdd79f1925278451771b14ab4ad4d Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 2 Jul 2019 11:12:53 +0100 Subject: [PATCH 100/209] Switch to "TLS" SSLContext to avoid blocking TLS1.3 --- .../com/manning/apisecurityinaction/token/OAuth2TokenStore.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java index 388c0ae..fd7a70f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java @@ -62,7 +62,7 @@ public OAuth2TokenStore(URI introspectionEndpoint, var tmf = TrustManagerFactory.getInstance("PKIX"); tmf.init(trustedCerts); - var sslContext = SSLContext.getInstance("TLSv1.2"); + var sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, tmf.getTrustManagers(), null); this.httpClient = HttpClient.newBuilder() From 683f41458cf9f72e77bff05fe0c4154bbc41b683 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 3 Jul 2019 10:48:35 +0100 Subject: [PATCH 101/209] Add SignedJwtAccessTokenStore --- .../com/manning/apisecurityinaction/Main.java | 14 +--- .../token/SignedJwtAccessTokenStore.java | 78 +++++++++++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index fca12fa..e1579be 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -3,6 +3,7 @@ import com.google.common.util.concurrent.RateLimiter; import com.manning.apisecurityinaction.controller.*; import com.manning.apisecurityinaction.token.*; +import com.nimbusds.jose.JWSAlgorithm; import org.dalesbred.Database; import org.dalesbred.result.EmptyResultException; import org.h2.jdbcx.JdbcConnectionPool; @@ -70,17 +71,10 @@ public static void main(String... args) throws Exception { .put("alg", "HS256") .put("typ", "JWT"); - var clientId = "test"; - var clientSecret = - System.getProperty("client_secret", "password"); - var introspectionEndpoint = - URI.create("https://as.example.com:8443/oauth2/introspect"); - var revocationEndpoint = - URI.create("https://as.example.com:8443/oauth2/token/revoke"); - SecureTokenStore tokenStore = new OAuth2TokenStore( - introspectionEndpoint, revocationEndpoint, - clientId, clientSecret); + var issuer = "https://openam.example.com:8443/openam/oauth2"; + var jwksUri = URI.create("http://openam.example.com:8080/openam/oauth2/connect/jwk_uri"); + var tokenStore = new SignedJwtAccessTokenStore(issuer, "test", jwksUri, JWSAlgorithm.ES256); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java new file mode 100644 index 0000000..8cc5f96 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java @@ -0,0 +1,78 @@ +package com.manning.apisecurityinaction.token; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.jwk.source.*; +import com.nimbusds.jose.proc.*; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import spark.Request; + +import java.net.*; +import java.text.ParseException; +import java.util.Optional; + +public class SignedJwtAccessTokenStore implements SecureTokenStore { + + private final String expectedIssuer; + private final String expectedAudience; + private final JWSAlgorithm signatureAlgorithm; + private final JWKSource jwkSource; + + public SignedJwtAccessTokenStore(String expectedIssuer, + String expectedAudience, + JWSAlgorithm signatureAlgorithm, + URI jwkSetUri) + throws MalformedURLException { + this.expectedIssuer = expectedIssuer; + this.expectedAudience = expectedAudience; + this.signatureAlgorithm = signatureAlgorithm; + this.jwkSource = new RemoteJWKSet<>(jwkSetUri.toURL()); + } + + @Override + public String create(Request request, Token token) { + throw new UnsupportedOperationException(); + } + + @Override + public void revoke(Request request, String tokenId) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional read(Request request, String tokenId) { + try { + var verifier = new DefaultJWTProcessor<>(); + var keySelector = new JWSVerificationKeySelector<>( + signatureAlgorithm, jwkSource); + verifier.setJWSKeySelector(keySelector); + + var claims = verifier.process(tokenId, null); + + if (!expectedIssuer.equals(claims.getIssuer())) { + return Optional.empty(); + } + if (!claims.getAudience().contains(expectedAudience)) { + return Optional.empty(); + } + + var expiry = claims.getExpirationTime().toInstant(); + var subject = claims.getSubject(); + + var token = new Token(expiry, subject); + + String scope; + try { + scope = claims.getStringClaim("scope"); + } catch (ParseException e) { + scope = String.join(" ", claims.getStringListClaim("scope")); + } + + token.attributes.put("scope", scope); + + return Optional.of(token); + + } catch (ParseException | BadJOSEException | JOSEException e) { + return Optional.empty(); + } + } +} From 7af8009f45028b55015bf70d942264727782117e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 3 Jul 2019 11:11:12 +0100 Subject: [PATCH 102/209] Fix compilation error --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index e1579be..3c8909f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -74,7 +74,7 @@ public static void main(String... args) throws Exception { var issuer = "https://openam.example.com:8443/openam/oauth2"; var jwksUri = URI.create("http://openam.example.com:8080/openam/oauth2/connect/jwk_uri"); - var tokenStore = new SignedJwtAccessTokenStore(issuer, "test", jwksUri, JWSAlgorithm.ES256); + var tokenStore = new SignedJwtAccessTokenStore(issuer, "test", JWSAlgorithm.ES256, jwksUri); var tokenController = new TokenController(tokenStore); before(userController::authenticate); From cacec3a8ad980504628b60184977aa08d51926ec Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 5 Jul 2019 13:28:46 +0100 Subject: [PATCH 103/209] Add a minimal OAuth 2 Authorization Server (ROPC only) --- .../com/manning/apisecurityinaction/Main.java | 17 ++- .../AuthorizationServerController.java | 119 ++++++++++++++++++ .../controller/UserController.java | 3 +- 3 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuthorizationServerController.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 3c8909f..ac821c7 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -17,7 +17,7 @@ import java.nio.file.*; import java.security.KeyStore; import java.sql.Connection; -import java.util.Set; +import java.util.*; import static spark.Service.SPARK_DEFAULT_PORT; import static spark.Spark.*; @@ -52,6 +52,7 @@ public static void main(String... args) throws Exception { before(((request, response) -> { if (request.requestMethod().equals("POST") && + !request.pathInfo().equals("/oauth2/access_token") && !"application/json".equals(request.contentType())) { halt(406, new JSONObject().put( "error", "Only application/json supported" @@ -77,6 +78,18 @@ public static void main(String... args) throws Exception { var tokenStore = new SignedJwtAccessTokenStore(issuer, "test", JWSAlgorithm.ES256, jwksUri); var tokenController = new TokenController(tokenStore); + var accessTokenStore = new DatabaseTokenStore(database); + var secretHash = Base64.getEncoder().encodeToString( + AuthorizationServerController.hash("password")); + var client = new JSONObject() + .put("secret_hash", secretHash) + .put("allowed_scope", List.of("create_space", "post_message")); + var clients = new JSONObject() + .put("test", client); + + var oauthController = new AuthorizationServerController( + accessTokenStore, database, clients); + before(userController::authenticate); before(tokenController::validateToken); @@ -90,6 +103,8 @@ public static void main(String... args) throws Exception { post("/sessions", tokenController::login); delete("/sessions", tokenController::logout); + post("/oauth2/access_token", oauthController::issueAccessToken); + get("/logs", auditController::readAuditLog); post("/users", userController::registerUser); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuthorizationServerController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuthorizationServerController.java new file mode 100644 index 0000000..4f8800c --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuthorizationServerController.java @@ -0,0 +1,119 @@ +package com.manning.apisecurityinaction.controller; + +import com.lambdaworks.crypto.SCryptUtil; +import com.manning.apisecurityinaction.token.*; +import org.dalesbred.Database; +import org.json.JSONObject; +import spark.*; + +import java.security.*; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class AuthorizationServerController { + + private final SecureTokenStore tokenStore; + private final Database database; + private final JSONObject clientConfig; + + public AuthorizationServerController(SecureTokenStore tokenStore, + Database database, + JSONObject clientConfig) { + this.tokenStore = tokenStore; + this.database = database; + this.clientConfig = clientConfig; + } + + public JSONObject issueAccessToken(Request request, Response response) { + + var grantType = request.queryMap("grant_type").value(); + if (!"password".equals(grantType)) { + throw new IllegalArgumentException("unsupported_grant_type"); + } + + var client = authenticateClient(request); + var username = request.queryMap("username").value(); + var password = request.queryMap("password").value(); + var scope = request.queryMap("scope").value(); + + if (scope == null || scope.isBlank() || + username == null || username.isBlank() || + password == null || password.isBlank()) { + throw new IllegalArgumentException("invalid_request"); + } + + if (!username.matches(UserController.USERNAME_PATTERN)) { + throw new IllegalArgumentException("invalid_request"); + } + scope = validateScope(scope, client); + + var hash = database.findOptional(String.class, + "SELECT pw_hash FROM users WHERE user_id = ?", username); + + if (hash.isPresent() && SCryptUtil.check(password, hash.get())) { + var expiry = Instant.now().plus(1, ChronoUnit.HOURS); + var token = new TokenStore.Token(expiry, username); + token.attributes.put("scope", scope); + + var tokenId = tokenStore.create(request, token); + return new JSONObject() + .put("access_token", tokenId) + .put("token_type", "Bearer") + .put("expires_in", 3600) + .put("scope", scope); + } else { + throw new IllegalArgumentException("invalid_grant"); + } + } + + private String validateScope(String scope, JSONObject client) { + var allowedScope = client.getJSONArray("allowed_scope").toList(); + var requestScope = scope.split(" "); + + var resultScope = new TreeSet(); + for (var requested : requestScope) { + if (allowedScope.contains(requested)) { + resultScope.add(requested); + } + } + + return String.join(" ", resultScope); + } + + private JSONObject authenticateClient(Request request) { + var clientId = request.queryMap("client_id").value(); + var secret = request.queryMap("client_secret").value(); + + if (clientId == null || clientId.isBlank() || + secret == null || secret.isBlank()) { + throw new IllegalArgumentException("invalid_client"); + } + + var client = clientConfig.optJSONObject(clientId); + if (client == null) { + throw new IllegalArgumentException("invalid_client"); + } + + var expected = Base64.getDecoder().decode( + client.getString("secret_hash")); + var provided = hash(secret); + + if (!MessageDigest.isEqual(expected, provided)) { + throw new IllegalArgumentException("invalid_client"); + } + + return client; + } + + public static byte[] hash(String clientSecret) { + try { + var sha256 = MessageDigest.getInstance("SHA-256"); + return sha256.digest(clientSecret.getBytes(UTF_8)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 825f3a0..5b0c09a 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -13,8 +13,7 @@ import spark.*; public class UserController { - private static final String USERNAME_PATTERN = - "[a-zA-Z][a-zA-Z0-9]{1,29}"; + static final String USERNAME_PATTERN = "[a-zA-Z][a-zA-Z0-9]{1,29}"; private final Database database; From 0ea1bbdb6194eeb7947b644e07315b81baa3d983 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 5 Jul 2019 14:35:27 +0100 Subject: [PATCH 104/209] Example filter for validating an ID token based on an access token. --- .../controller/IdTokenValidationFilter.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/IdTokenValidationFilter.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/IdTokenValidationFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/IdTokenValidationFilter.java new file mode 100644 index 0000000..8df029d --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/IdTokenValidationFilter.java @@ -0,0 +1,72 @@ +package com.manning.apisecurityinaction.controller; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.*; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import spark.*; + +import java.text.ParseException; + +public class IdTokenValidationFilter implements Filter { + + private final String expectedIssuer; + private final String expectedAudience; + private final JWSAlgorithm signatureAlgorithm; + private final JWKSource jwkSource; + + public IdTokenValidationFilter(String expectedIssuer, + String expectedAudience, + JWSAlgorithm signatureAlgorithm, + JWKSource jwkSource) { + this.expectedIssuer = expectedIssuer; + this.expectedAudience = expectedAudience; + this.signatureAlgorithm = signatureAlgorithm; + this.jwkSource = jwkSource; + } + + @Override + public void handle(Request request, Response response) { + + var idToken = request.headers("X-ID-Token"); + if (idToken == null) return; + var subject = request.attribute("subject"); + if (subject == null) return; + + var verifier = new DefaultJWTProcessor<>(); + var keySelector = new JWSVerificationKeySelector<>( + signatureAlgorithm, jwkSource); + verifier.setJWSKeySelector(keySelector); + + try { + var claims = verifier.process(idToken, null); + + if (!expectedIssuer.equals(claims.getIssuer())) { + throw new IllegalArgumentException( + "invalid id token issuer"); + } + if (!claims.getAudience().contains(expectedAudience)) { + throw new IllegalArgumentException( + "invalid id token audience"); + } + + var client = request.attribute("client_id"); + var azp = claims.getStringClaim("azp"); + if (client != null && azp != null && !azp.equals(client)) { + throw new IllegalArgumentException( + "client is not authorized party"); + } + + if (!subject.equals(claims.getSubject())) { + throw new IllegalArgumentException( + "subject does not match id token"); + } + + request.attribute("id_token.claims", claims); + + } catch (ParseException | BadJOSEException | JOSEException e) { + throw new IllegalArgumentException("invalid id token", e); + } + + } +} From c9b2cac814c1da0c858a3dcc37a7b5d99cfcef9e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 1 Aug 2019 13:34:41 +0100 Subject: [PATCH 105/209] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1062418..058db06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea/ *.iml +target/ +*.zip From e68d31233f02da562c03d7e85a6a4aeb8340479f Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 5 Aug 2019 21:15:58 +0100 Subject: [PATCH 106/209] Use static import for now() --- .../controller/TokenController.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java index fc243f8..ee51a0b 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java @@ -1,14 +1,13 @@ package com.manning.apisecurityinaction.controller; -import java.time.Instant; import java.time.temporal.ChronoUnit; -import org.json.JSONObject; - import com.manning.apisecurityinaction.token.TokenStore; - +import org.json.JSONObject; import spark.*; +import static java.time.Instant.now; + public class TokenController { private final TokenStore tokenStore; @@ -19,7 +18,7 @@ public TokenController(TokenStore tokenStore) { public JSONObject login(Request request, Response response) { String subject = request.attribute("subject"); - var expiry = Instant.now().plus(10, ChronoUnit.MINUTES); + var expiry = now().plus(10, ChronoUnit.MINUTES); var token = new TokenStore.Token(expiry, subject); var tokenId = tokenStore.create(request, token); @@ -34,7 +33,7 @@ public void validateToken(Request request, Response response) { if (tokenId == null) return; tokenStore.read(request, tokenId).ifPresent(token -> { - if (Instant.now().isBefore(token.expiry)) { + if (now().isBefore(token.expiry)) { request.attribute("subject", token.username); token.attributes.forEach(request::attribute); } From 25b6bdfe9243398e60e525a5359fa469fd4ec627 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 1 Aug 2019 13:34:41 +0100 Subject: [PATCH 107/209] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1062418..058db06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea/ *.iml +target/ +*.zip From 67a175405924a8b4aff339e9c0e7b322cc26df38 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 1 Aug 2019 13:34:41 +0100 Subject: [PATCH 108/209] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1062418..058db06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea/ *.iml +target/ +*.zip From 70627f00813b659cbb4e8e776e9b9d9d60f0b553 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 1 Aug 2019 13:34:41 +0100 Subject: [PATCH 109/209] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1062418..058db06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea/ *.iml +target/ +*.zip From 492f42349f1bb588cc7887fbec928a68c660f4b8 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 1 Aug 2019 13:34:41 +0100 Subject: [PATCH 110/209] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1062418..058db06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea/ *.iml +target/ +*.zip From 613be246a32f8723cad6a35225202782cc14b104 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 1 Aug 2019 13:34:41 +0100 Subject: [PATCH 111/209] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1062418..058db06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea/ *.iml +target/ +*.zip From 2235316a7cdf86f3ced110a8c3e1d6ada18a88e3 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 1 Aug 2019 13:34:41 +0100 Subject: [PATCH 112/209] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 1062418..058db06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea/ *.iml +target/ +*.zip From de85494c3de4e3fffdea656093d9664ceefdb2a4 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 10 Sep 2019 11:19:38 +0100 Subject: [PATCH 113/209] Update dependencies to latest stable versions --- natter-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/natter-api/pom.xml b/natter-api/pom.xml index cda8f7b..5c29668 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -21,23 +21,23 @@ com.sparkjava spark-core - 2.9.0 + 2.9.1 org.json json 20180813 - - org.slf4j - slf4j-simple - 1.7.21 - org.dalesbred dalesbred 1.3.0 + + org.slf4j + slf4j-simple + 1.7.26 + com.google.guava guava From 765774e32404ecb2b1ac6e85b10c777466e6e826 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 10 Sep 2019 11:19:38 +0100 Subject: [PATCH 114/209] Update dependencies to latest stable versions --- natter-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/natter-api/pom.xml b/natter-api/pom.xml index cda8f7b..5c29668 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -21,23 +21,23 @@ com.sparkjava spark-core - 2.9.0 + 2.9.1 org.json json 20180813 - - org.slf4j - slf4j-simple - 1.7.21 - org.dalesbred dalesbred 1.3.0 + + org.slf4j + slf4j-simple + 1.7.26 + com.google.guava guava From e9fef506b6b4eb56ef30a942c505f2aaf9190849 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 10 Sep 2019 11:19:38 +0100 Subject: [PATCH 115/209] Update dependencies to latest stable versions --- natter-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/natter-api/pom.xml b/natter-api/pom.xml index cda8f7b..5c29668 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -21,23 +21,23 @@ com.sparkjava spark-core - 2.9.0 + 2.9.1 org.json json 20180813 - - org.slf4j - slf4j-simple - 1.7.21 - org.dalesbred dalesbred 1.3.0 + + org.slf4j + slf4j-simple + 1.7.26 + com.google.guava guava From e5db6eccd6d84b05519aff8c6e02711d932505f1 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 10 Sep 2019 11:19:38 +0100 Subject: [PATCH 116/209] Update dependencies to latest stable versions --- natter-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/natter-api/pom.xml b/natter-api/pom.xml index cda8f7b..5c29668 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -21,23 +21,23 @@ com.sparkjava spark-core - 2.9.0 + 2.9.1 org.json json 20180813 - - org.slf4j - slf4j-simple - 1.7.21 - org.dalesbred dalesbred 1.3.0 + + org.slf4j + slf4j-simple + 1.7.26 + com.google.guava guava From 3ae56eb74d2d8e6dde87a213164249f115af91ec Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 10 Sep 2019 11:19:38 +0100 Subject: [PATCH 117/209] Update dependencies to latest stable versions --- natter-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/natter-api/pom.xml b/natter-api/pom.xml index 84895e0..46778f7 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -21,23 +21,23 @@ com.sparkjava spark-core - 2.9.0 + 2.9.1 org.json json 20180813 - - org.slf4j - slf4j-simple - 1.7.21 - org.dalesbred dalesbred 1.3.0 + + org.slf4j + slf4j-simple + 1.7.26 + com.google.guava guava From 8ee0540b3de13f53c1eb21f3736e38c8f6d390b5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 10 Sep 2019 11:19:38 +0100 Subject: [PATCH 118/209] Update dependencies to latest stable versions --- natter-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/natter-api/pom.xml b/natter-api/pom.xml index 84895e0..46778f7 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -21,23 +21,23 @@ com.sparkjava spark-core - 2.9.0 + 2.9.1 org.json json 20180813 - - org.slf4j - slf4j-simple - 1.7.21 - org.dalesbred dalesbred 1.3.0 + + org.slf4j + slf4j-simple + 1.7.26 + com.google.guava guava From 6537bd262908b5ce55e4d64635af3e353c573114 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 1 Aug 2019 15:48:07 +0100 Subject: [PATCH 119/209] Update default security headers --- .../main/java/com/manning/apisecurityinaction/Main.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 9a13f39..2690120 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -46,7 +46,7 @@ public static void main(String... args) throws Exception { before(((request, response) -> { if (request.requestMethod().equals("POST") && !"application/json".equals(request.contentType())) { - halt(406, new JSONObject().put( + halt(415, new JSONObject().put( "error", "Only application/json supported" ).toString()); } @@ -99,10 +99,13 @@ public static void main(String... args) throws Exception { moderatorController::deletePost); afterAfter((request, response) -> { - response.type("application/json"); + response.type("application/json; charset=utf-8"); response.header("X-Content-Type-Options", "nosniff"); + response.header("X-Frame-Options", "deny"); response.header("X-XSS-Protection", "1; mode=block"); response.header("Cache-Control", "private, max-age=0"); + response.header("Content-Security-Policy", + "default-src 'none'; frame-ancestors 'none'; sandbox"); response.header("Server", ""); }); From af7163a94e682a96e3b42f0603ae83be78abbdb9 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 1 Aug 2019 15:48:07 +0100 Subject: [PATCH 120/209] Update default security headers --- .../main/java/com/manning/apisecurityinaction/Main.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index fb12d7b..8ad0c38 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -51,7 +51,7 @@ public static void main(String... args) throws Exception { before(((request, response) -> { if (request.requestMethod().equals("POST") && !"application/json".equals(request.contentType())) { - halt(406, new JSONObject().put( + halt(415, new JSONObject().put( "error", "Only application/json supported" ).toString()); } @@ -112,10 +112,13 @@ public static void main(String... args) throws Exception { moderatorController::deletePost); afterAfter((request, response) -> { - response.type("application/json"); + response.type("application/json; charset=utf-8"); response.header("X-Content-Type-Options", "nosniff"); + response.header("X-Frame-Options", "deny"); response.header("X-XSS-Protection", "1; mode=block"); response.header("Cache-Control", "private, max-age=0"); + response.header("Content-Security-Policy", + "default-src 'none'; frame-ancestors 'none'; sandbox"); response.header("Server", ""); }); From 9770aac417f475d1e7b3ec93c1692904367f5410 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 1 Aug 2019 15:48:07 +0100 Subject: [PATCH 121/209] Update default security headers --- .../main/java/com/manning/apisecurityinaction/Main.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index d95c024..376bef7 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -52,7 +52,7 @@ public static void main(String... args) throws Exception { before(((request, response) -> { if (request.requestMethod().equals("POST") && !"application/json".equals(request.contentType())) { - halt(406, new JSONObject().put( + halt(415, new JSONObject().put( "error", "Only application/json supported" ).toString()); } @@ -132,10 +132,13 @@ public static void main(String... args) throws Exception { moderatorController::deletePost); afterAfter((request, response) -> { - response.type("application/json"); + response.type("application/json; charset=utf-8"); response.header("X-Content-Type-Options", "nosniff"); + response.header("X-Frame-Options", "deny"); response.header("X-XSS-Protection", "1; mode=block"); response.header("Cache-Control", "private, max-age=0"); + response.header("Content-Security-Policy", + "default-src 'none'; frame-ancestors 'none'; sandbox"); response.header("Server", ""); }); From 61349748913eeb2e3b1092e445592d15f95db5f0 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 11 Sep 2019 11:04:05 +0100 Subject: [PATCH 122/209] Add OAuth2TokenStore --- .../com/manning/apisecurityinaction/Main.java | 12 ++- .../token/OAuth2TokenStore.java | 96 +++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 376bef7..3ae4dc3 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -3,6 +3,7 @@ import com.google.common.util.concurrent.RateLimiter; import com.manning.apisecurityinaction.controller.*; import com.manning.apisecurityinaction.token.*; +import org.checkerframework.checker.units.qual.A; import org.dalesbred.Database; import org.dalesbred.result.EmptyResultException; import org.h2.jdbcx.JdbcConnectionPool; @@ -13,6 +14,7 @@ import javax.crypto.SecretKey; import java.io.FileInputStream; +import java.net.URI; import java.nio.file.*; import java.security.KeyStore; import java.sql.Connection; @@ -70,10 +72,12 @@ public static void main(String... args) throws Exception { .put("alg", "HS256") .put("typ", "JWT"); - var tokenWhitelist = new DatabaseTokenStore(database); - SecureTokenStore tokenStore = - new JwtTokenStore((SecretKey) encKey, tokenWhitelist); - + var clientId = "testClient"; + var clientSecret = "60ho9IS3d6/A+Zzvdn9Y4laiGnI/1TddTM95lEHjArw="; + var introspectionEndpoint = + URI.create("https://as.example.com:8443/oauth2/introspect"); + SecureTokenStore tokenStore = new OAuth2TokenStore( + introspectionEndpoint, clientId, clientSecret); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java new file mode 100644 index 0000000..43b0bb1 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java @@ -0,0 +1,96 @@ +package com.manning.apisecurityinaction.token; + +import java.io.IOException; +import java.net.*; +import java.net.http.*; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; +import java.time.Instant; +import java.util.*; + +import org.json.JSONObject; +import spark.Request; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class OAuth2TokenStore implements SecureTokenStore { + + private final URI introspectionEndpoint; + private final String authorization; + + private final HttpClient httpClient; + + public OAuth2TokenStore(URI introspectionEndpoint, + String clientId, String clientSecret) { + this.introspectionEndpoint = introspectionEndpoint; + + var credentials = URLEncoder.encode(clientId, UTF_8) + ":" + + URLEncoder.encode(clientSecret, UTF_8); + this.authorization = "Basic " + Base64.getEncoder() + .encodeToString(credentials.getBytes(UTF_8)); + + this.httpClient = HttpClient.newHttpClient(); + } + + @Override + public String create(Request request, Token token) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional read(Request request, String tokenId) { + if (!tokenId.matches("[\\x20-\\x7E]{1,1024}")) { + return Optional.empty(); + } + + var form = "token=" + URLEncoder.encode(tokenId, UTF_8) + + "&token_type_hint=access_token"; + + var httpRequest = HttpRequest.newBuilder() + .uri(introspectionEndpoint) + .header("Content-Type", + "application/x-www-form-urlencoded") + .header("Authorization", authorization) + .POST(BodyPublishers.ofString(form)) + .build(); + + try { + var httpResponse = httpClient.send(httpRequest, + BodyHandlers.ofString()); + + if (httpResponse.statusCode() == 200) { + var json = new JSONObject(httpResponse.body()); + + if (json.getBoolean("active")) { + return processResponse(json); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + + return Optional.empty(); + } + + private Optional processResponse(JSONObject response) { + var expiry = Instant.ofEpochSecond(response.getLong("exp")); + var subject = response.getString("sub"); + + var token = new Token(expiry, subject); + + token.attributes.put("scope", response.getString("scope")); + token.attributes.put("client_id", + response.optString("client_id")); + + return Optional.of(token); + } + + + @Override + public void revoke(Request request, String tokenId) { + throw new UnsupportedOperationException(); + } +} From 384e4e09c3072539b641c4df1c7e4adc409690e6 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 11 Sep 2019 11:15:31 +0100 Subject: [PATCH 123/209] Secure HTTPS client settings --- natter-api/as.example.com.ca.p12 | Bin 0 -> 1506 bytes .../token/OAuth2TokenStore.java | 41 +++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 natter-api/as.example.com.ca.p12 diff --git a/natter-api/as.example.com.ca.p12 b/natter-api/as.example.com.ca.p12 new file mode 100644 index 0000000000000000000000000000000000000000..d8c20d2015c27d90034fbad2f8bdba54b6df148a GIT binary patch literal 1506 zcmV<81s(b@f(70J0Ru3C1(ya1Duzgg_YDCD0ic2fhy;QKgfM~yfG~mudf@^5iTk2T`FeGD9|F7bHqVn8VbOMrAFjYo`km8( zJvb8=$W;W2RaOeG%g4{aq3`7`1r8pF{L3yBFRuK>y=lsiP!Mho(US0q3k2vcI|s=`|{e zX7(0?lpt!9ruO|}IFBam8!C4zT}I;w(k0u6U-He_=%@ttFi%Z0^GN6o(gX5FzM zcCNDU8>bs|nDRZd?Mxcqs;Vb}LCQKas<;T__*=h)_>LdGSh~wBcCL`;xTd@VcRbb} zgA8nUE2ETBz8!}J|F%{fDm^Vg$~qhQX6912@lcWXUD)N5nbOSeRfQuo-q^v;BtRSS zKPSNv3n%w(ZW9INFbPxbnZrGf=pfaUPhzIIbNw(v13MNrTw1R9^!;P`ysIDHB{-?U z6FYIp{}bR;nY>r&*SH+rc3xSNWoR07l_>$jBUwxQS?@*I=*xQT@6z>ME#AU=EZ@T% zBZlW47>d2JnJO?tZ7Bq74=M(~;`%6fxuFhPwp0V#_J=={c z1iTf&Z|XC=3uj!{T}AtLXRpq(n4NfSm1{NAUG)^JpaKB;XXhk$^fkG9Hs0Wv6E|}0 zvm9HWT@w4+Vc5a0I@VyU!cxjSf>1Z7>#HranNZnd(s8H=@L@lt(^iU2tY!l)TjG!futm#f^(iLq4EEwI z2H$3;QhH3`p2`I}Nj)<|hLsPV`9a>IToc3}zI{_uXQ)F+Es?e#3aVsGSXs&Jgf+M^ ze6BQp5#|TI63)~%Gq{)T?sw1>V4_bXS(6ds?hRTdwSL1ly~N z1*%x1tIR^|PUN9*P>2n;yetQ@d?IrAF5ph)zukkK*V4#5P|i&XohwEK=FeQ2%+$z#De_S4>pHT=xr(-#-tM&wK!Nn11SAf$4^mWP{AR^oL2g9&vt>-N#9>%!nw5io8CfsTG^gA;wetO+Na~Y=>TQhQ< zq^5-D6N}*&=bB`^T$Ii8NUT{?>*B2x)V9QeVi@fIRENIZ*aZ(N%r|$4GmGNdrv!ac z!lga10hQYQq(8EVuXY>Sbm$nnfk0Roo@H54&l+qM1BexE!oYX@;A6^_cc0eR&S?)D z4m<#laF1p~SA{q(4C6{V&mUMwBKzqns}+a4e@GO*&GBmN_TW%GmJMPt1we5}>)@ei z`eppng}LJAVf2cKrDx!jP^)W-f!tA><5Sjs#*wbNTA#-b7P?@Mm{iiHaxgwHAutIB z1uG5%0vZJX1Qhb*rKqI((Ds&Vn>Zg#Kc;Ff=WYZPRYmC@Clom}lT+dUAe5L1%pVD- I0s{etpe5ti2><{9 literal 0 HcmV?d00001 diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java index 43b0bb1..4f63a3b 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java @@ -1,10 +1,12 @@ package com.manning.apisecurityinaction.token; -import java.io.IOException; +import javax.net.ssl.*; +import java.io.*; import java.net.*; import java.net.http.*; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandlers; +import java.security.*; import java.time.Instant; import java.util.*; @@ -29,7 +31,42 @@ public OAuth2TokenStore(URI introspectionEndpoint, this.authorization = "Basic " + Base64.getEncoder() .encodeToString(credentials.getBytes(UTF_8)); - this.httpClient = HttpClient.newHttpClient(); + var sslParams = new SSLParameters(); + sslParams.setProtocols(new String[] { + // TLS 1.3 cipher suites + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + + // TLS 1.2 cipher suites + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" + }); + sslParams.setUseCipherSuitesOrder(true); + sslParams.setEndpointIdentificationAlgorithm("HTTPS"); + + try { + var trustedCerts = KeyStore.getInstance("PKCS12"); + trustedCerts.load( + new FileInputStream("as.example.com.ca.p12"), + "changeit".toCharArray()); + var tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(trustedCerts); + var sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + + this.httpClient = HttpClient.newBuilder() + .sslParameters(sslParams) + .sslContext(sslContext) + .build(); + + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException(e); + } } @Override From 5ac796e1a9a9d460ff945d1d03f10252cb2b7305 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 11 Sep 2019 11:17:07 +0100 Subject: [PATCH 124/209] Add simple app for revoking access tokens --- .../RevokeAccessToken.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java b/natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java new file mode 100644 index 0000000..a1e827e --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java @@ -0,0 +1,47 @@ +package com.manning.apisecurityinaction; + +import java.net.*; +import java.net.http.*; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Base64; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class RevokeAccessToken { + + private static final URI revocationEndpoint = + URI.create("https://as.example.com:8443/oauth2/token/revoke"); + + public static void main(String...args) throws Exception { + + if (args.length != 3) { + throw new IllegalArgumentException( + "RevokeAccessToken clientId clientSecret token"); + } + + var clientId = args[0]; + var clientSecret = args[1]; + var token = args[2]; + + var credentials = URLEncoder.encode(clientId, UTF_8) + + + ":" + URLEncoder.encode(clientSecret, UTF_8); + var authorization = "Basic " + Base64.getEncoder() + .encodeToString(credentials.getBytes(UTF_8)); + + var httpClient = HttpClient.newHttpClient(); + + var form = "token=" + URLEncoder.encode(token, UTF_8) + + "&token_type_hint=access_token"; + + var httpRequest = HttpRequest.newBuilder() + .uri(revocationEndpoint) + .header("Content-Type", + "application/x-www-form-urlencoded") + .header("Authorization", authorization) + .POST(HttpRequest.BodyPublishers.ofString(form)) + .build(); + + httpClient.send(httpRequest, BodyHandlers.discarding()); + } +} From dd608c5b5996fc83aacfbb59b2c9f37f4d3b44c0 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 11 Sep 2019 11:23:13 +0100 Subject: [PATCH 125/209] Add SignedJwtAccessTokenStore --- .../token/SignedJwtAccessTokenStore.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java new file mode 100644 index 0000000..2cedf0b --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java @@ -0,0 +1,76 @@ +package com.manning.apisecurityinaction.token; + +import java.net.*; +import java.text.ParseException; +import java.util.Optional; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.jwk.source.*; +import com.nimbusds.jose.proc.*; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import spark.Request; + +public class SignedJwtAccessTokenStore implements SecureTokenStore { + + private final String expectedIssuer; + private final String expectedAudience; + private final JWSAlgorithm signatureAlgorithm; + private final JWKSource jwkSource; + + public SignedJwtAccessTokenStore(String expectedIssuer, + String expectedAudience, + JWSAlgorithm signatureAlgorithm, + URI jwkSetUri) + throws MalformedURLException { + this.expectedIssuer = expectedIssuer; + this.expectedAudience = expectedAudience; + this.signatureAlgorithm = signatureAlgorithm; + this.jwkSource = new RemoteJWKSet<>(jwkSetUri.toURL()); + } + + @Override + public String create(Request request, Token token) { + throw new UnsupportedOperationException(); + } + + @Override + public void revoke(Request request, String tokenId) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional read(Request request, String tokenId) { + try { + var verifier = new DefaultJWTProcessor<>(); + var keySelector = new JWSVerificationKeySelector<>( + signatureAlgorithm, jwkSource); + verifier.setJWSKeySelector(keySelector); + + var claims = verifier.process(tokenId, null); + + if (!expectedIssuer.equals(claims.getIssuer())) { + return Optional.empty(); + } + if (!claims.getAudience().contains(expectedAudience)) { + return Optional.empty(); + } + + var expiry = claims.getExpirationTime().toInstant(); + var subject = claims.getSubject(); + var token = new Token(expiry, subject); + + String scope; + try { + scope = claims.getStringClaim("scope"); + } catch (ParseException e) { + scope = String.join(" ", + claims.getStringListClaim("scope")); + } + token.attributes.put("scope", scope); + return Optional.of(token); + + } catch (ParseException | BadJOSEException | JOSEException e) { + return Optional.empty(); + } + } +} From 8102eba39bcb86eb3dfed50b8b99efd7781d93d3 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 14 Sep 2019 11:39:01 +0100 Subject: [PATCH 126/209] Add simple group support --- .../controller/SpaceController.java | 4 +-- .../controller/UserController.java | 32 +++++++++++++------ natter-api/src/main/resources/schema.sql | 11 +++++-- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index b8db8df..4cb543b 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -42,7 +42,7 @@ public JSONObject createSpace(Request request, Response response) { "VALUES(?, ?, ?);", spaceId, spaceName, owner); database.updateUnique( - "INSERT INTO permissions(space_id, user_id, perms) " + + "INSERT INTO permissions(space_id, user_or_group_id, perms) " + "VALUES(?, ?, ?)", spaceId, owner, "rwd"); response.status(201); @@ -128,7 +128,7 @@ public JSONObject addMember(Request request, Response response) { } database.updateUnique( - "INSERT INTO permissions(space_id, user_id, perms) " + + "INSERT INTO permissions(space_id, user_or_group_id, perms) " + "VALUES(?, ?, ?)", spaceId, userToAdd, perms); response.status(200); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 825f3a0..7c7bf5d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -1,17 +1,16 @@ package com.manning.apisecurityinaction.controller; -import static spark.Spark.halt; - import java.nio.charset.StandardCharsets; -import java.util.Base64; +import java.util.*; +import com.lambdaworks.crypto.SCryptUtil; import org.dalesbred.Database; +import org.dalesbred.query.QueryBuilder; import org.json.JSONObject; - -import com.lambdaworks.crypto.SCryptUtil; - import spark.*; +import static spark.Spark.halt; + public class UserController { private static final String USERNAME_PATTERN = "[a-zA-Z][a-zA-Z0-9]{1,29}"; @@ -73,6 +72,11 @@ public void authenticate(Request request, Response response) { if (hash.isPresent() && SCryptUtil.check(password, hash.get())) { request.attribute("subject", username); + + var groups = database.findAll(String.class, + "SELECT DISTINCT group_id FROM group_members " + + "WHERE user_id = ?", username); + request.attribute("groups", groups); } } @@ -93,13 +97,21 @@ public Filter requirePermission(String method, String permission) { var spaceId = Long.parseLong(request.params(":spaceId")); var username = (String) request.attribute("subject"); + List groups = request.attribute("groups"); - var perms = database.findOptional(String.class, + var queryBuilder = new QueryBuilder( "SELECT perms FROM permissions " + - "WHERE space_id = ? AND user_id = ?", - spaceId, username).orElse(""); + "WHERE space_id = ? " + + "AND (user_or_group_id = ?", spaceId, username); + + for (var group : groups) { + queryBuilder.append(" OR user_or_group_id = ?", group); + } + queryBuilder.append(")"); - if (!perms.contains(permission)) { + var perms = database.findAll(String.class, + queryBuilder.build()); + if (perms.stream().noneMatch(p -> p.contains(permission))) { halt(403); } }; diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index dfad384..0c24898 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -2,6 +2,12 @@ CREATE TABLE users( user_id VARCHAR(30) PRIMARY KEY, pw_hash VARCHAR(255) NOT NULL ); +CREATE TABLE group_members( + group_id VARCHAR(30), + user_id VARCHAR(30) REFERENCES users(user_id) +); +CREATE INDEX group_member_user_idx ON group_members(user_id); + CREATE TABLE spaces( space_id INT PRIMARY KEY, name VARCHAR(255) NOT NULL, @@ -31,9 +37,9 @@ CREATE SEQUENCE audit_id_seq; CREATE TABLE permissions( space_id INT NOT NULL REFERENCES spaces(space_id), - user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), + user_or_group_id VARCHAR(30) NOT NULL, perms VARCHAR(3) NOT NULL, - PRIMARY KEY (space_id, user_id) + PRIMARY KEY (space_id, user_or_group_id) ); CREATE TABLE tokens( @@ -51,3 +57,4 @@ GRANT SELECT, INSERT ON users TO natter_api_user; GRANT SELECT, INSERT ON audit_log TO natter_api_user; GRANT SELECT, INSERT ON permissions TO natter_api_user; GRANT SELECT, INSERT, DELETE ON tokens TO natter_api_user; +GRANT SELECT, INSERT, DELETE ON group_members TO natter_api_user; \ No newline at end of file From 6e0e11a7e7a62794f5514f22f978eec0b9d90c07 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 14 Sep 2019 20:10:48 +0100 Subject: [PATCH 127/209] Add LdapUserController that authenticates and looks up groups from LDAP --- .../com/manning/apisecurityinaction/Main.java | 7 +- .../controller/LdapUserController.java | 90 +++++++++++++++++++ .../controller/UserController.java | 36 +++++--- 3 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/LdapUserController.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 3ae4dc3..eebee5a 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -41,7 +41,12 @@ public static void main(String... args) throws Exception { var database = Database.forDataSource(datasource); var spaceController = new SpaceController(database); - var userController = new UserController(database); + + var ldapUrl = "ldap://localhost:50389/"; + var baseDn = "dc=openam,dc=forgerock,dc=org"; + + var userController = new LdapUserController(database, + ldapUrl, baseDn, "cn=Directory Manager", "cangetinam"); var rateLimiter = RateLimiter.create(2.0d); before((request, response) -> { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/LdapUserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/LdapUserController.java new file mode 100644 index 0000000..7d93dac --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/LdapUserController.java @@ -0,0 +1,90 @@ +package com.manning.apisecurityinaction.controller; + +import javax.naming.*; +import javax.naming.directory.*; +import java.util.*; + +import org.dalesbred.Database; +import org.json.JSONObject; +import org.slf4j.*; +import spark.*; + +public class LdapUserController extends UserController { + private static final Logger logger = + LoggerFactory.getLogger(LdapUserController.class); + + private final String ldapUrl; + private final String baseDn; + private final DirContext connection; + + public LdapUserController(Database database, String ldapUrl, + String baseDn, String connDn, + String connPassword) throws NamingException { + super(database); + this.ldapUrl = ldapUrl; + this.baseDn = baseDn; + this.connection = bind(connDn, connPassword); + } + + @Override + public JSONObject registerUser(Request request, Response response) { + throw new UnsupportedOperationException( + "Please register users in LDAP directly"); + } + + @Override + public void authenticate(Request request, Response response) { + var credentials = getCredentials(request); + if (credentials == null) return; + + var username = credentials[0]; + var password = credentials[1]; + + var dn = "uid=" + username + ",ou=people," + baseDn; + + try { + var directory = bind(dn, password); + // Authentication succeeded + request.attribute("subject", username); + directory.close(); + + // Lookup static groups for the user + var searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(new String[] { "cn" }); + + var groups = new ArrayList(); + var results = connection.search("ou=groups," + baseDn, + "(&(objectClass=groupOfNames)(member={0}))", + new Object[]{ dn }, + searchControls); + try { + while (results.hasMore()) { + var result = results.next(); + groups.add((String) result.getAttributes() + .get("cn").get(0)); + } + } finally { + results.close(); + } + request.attribute("groups", groups); + } catch (AuthenticationException e) { + logger.debug("Authentication failed for user {}", username, e); + } catch (NamingException e) { + throw new RuntimeException("Unable to login", e); + } + } + + private DirContext bind(String userDn, String password) + throws NamingException { + var props = new Properties(); + props.put(Context.INITIAL_CONTEXT_FACTORY, + "com.sun.jndi.ldap.LdapCtxFactory"); + props.put(Context.PROVIDER_URL, ldapUrl); + props.put(Context.SECURITY_AUTHENTICATION, "simple"); + props.put(Context.SECURITY_PRINCIPAL, userDn); + props.put(Context.SECURITY_CREDENTIALS, password); + + return new InitialDirContext(props); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 7c7bf5d..4dac87f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -46,9 +46,29 @@ public JSONObject registerUser(Request request, } public void authenticate(Request request, Response response) { + var credentials = getCredentials(request); + if (credentials == null) return; + + var username = credentials[0]; + var password = credentials[1]; + + var hash = database.findOptional(String.class, + "SELECT pw_hash FROM users WHERE user_id = ?", username); + + if (hash.isPresent() && SCryptUtil.check(password, hash.get())) { + request.attribute("subject", username); + + var groups = database.findAll(String.class, + "SELECT DISTINCT group_id FROM group_members " + + "WHERE user_id = ?", username); + request.attribute("groups", groups); + } + } + + String[] getCredentials(Request request) { var authHeader = request.headers("Authorization"); if (authHeader == null || !authHeader.startsWith("Basic ")) { - return; + return null; } var offset = "Basic ".length(); @@ -61,23 +81,11 @@ public void authenticate(Request request, Response response) { } var username = components[0]; - var password = components[1]; - if (!username.matches(USERNAME_PATTERN)) { throw new IllegalArgumentException("invalid username"); } - var hash = database.findOptional(String.class, - "SELECT pw_hash FROM users WHERE user_id = ?", username); - - if (hash.isPresent() && SCryptUtil.check(password, hash.get())) { - request.attribute("subject", username); - - var groups = database.findAll(String.class, - "SELECT DISTINCT group_id FROM group_members " + - "WHERE user_id = ?", username); - request.attribute("groups", groups); - } + return components; } public void requireAuthentication(Request request, Response response) { From 74ef806dd68ab392183da2e32d414617a7fc434b Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 14 Sep 2019 20:17:02 +0100 Subject: [PATCH 128/209] Revert to original UserController --- .../com/manning/apisecurityinaction/Main.java | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index eebee5a..51452d3 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -1,9 +1,15 @@ package com.manning.apisecurityinaction; +import java.io.FileInputStream; +import java.net.URI; +import java.nio.file.*; +import java.security.KeyStore; +import java.sql.Connection; +import java.util.Set; + import com.google.common.util.concurrent.RateLimiter; import com.manning.apisecurityinaction.controller.*; import com.manning.apisecurityinaction.token.*; -import org.checkerframework.checker.units.qual.A; import org.dalesbred.Database; import org.dalesbred.result.EmptyResultException; import org.h2.jdbcx.JdbcConnectionPool; @@ -12,14 +18,6 @@ import spark.embeddedserver.EmbeddedServers; import spark.embeddedserver.jetty.EmbeddedJettyFactory; -import javax.crypto.SecretKey; -import java.io.FileInputStream; -import java.net.URI; -import java.nio.file.*; -import java.security.KeyStore; -import java.sql.Connection; -import java.util.Set; - import static spark.Service.SPARK_DEFAULT_PORT; import static spark.Spark.*; @@ -41,12 +39,7 @@ public static void main(String... args) throws Exception { var database = Database.forDataSource(datasource); var spaceController = new SpaceController(database); - - var ldapUrl = "ldap://localhost:50389/"; - var baseDn = "dc=openam,dc=forgerock,dc=org"; - - var userController = new LdapUserController(database, - ldapUrl, baseDn, "cn=Directory Manager", "cangetinam"); + var userController = new UserController(database); var rateLimiter = RateLimiter.create(2.0d); before((request, response) -> { From 5e7008bfaca7ac5b11465ac47032e2de72505ca9 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 14 Sep 2019 22:36:28 +0100 Subject: [PATCH 129/209] Implement RBAC --- .../controller/SpaceController.java | 20 ++++++++-------- .../controller/UserController.java | 23 +++++++------------ natter-api/src/main/resources/schema.sql | 23 ++++++++++++++----- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index 4cb543b..a0fcc11 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -2,14 +2,16 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Set; import java.util.stream.Collectors; import org.dalesbred.Database; import org.json.*; - import spark.*; public class SpaceController { + private static final Set DEFINED_ROLES = + Set.of("owner", "moderator", "member", "observer"); private final Database database; @@ -42,8 +44,8 @@ public JSONObject createSpace(Request request, Response response) { "VALUES(?, ?, ?);", spaceId, spaceName, owner); database.updateUnique( - "INSERT INTO permissions(space_id, user_or_group_id, perms) " + - "VALUES(?, ?, ?)", spaceId, owner, "rwd"); + "INSERT INTO user_roles(space_id, user_id, role_id) " + + "VALUES(?, ?, ?)", spaceId, owner, "owner"); response.status(201); response.header("Location", "/spaces/" + spaceId); @@ -121,20 +123,20 @@ public JSONObject addMember(Request request, Response response) { var json = new JSONObject(request.body()); var spaceId = Long.parseLong(request.params(":spaceId")); var userToAdd = json.getString("username"); - var perms = json.getString("permissions"); + var role = json.optString("role", "member"); - if (!perms.matches("r?w?d?")) { - throw new IllegalArgumentException("invalid permissions"); + if (!DEFINED_ROLES.contains(role)) { + throw new IllegalArgumentException("invalid role"); } database.updateUnique( - "INSERT INTO permissions(space_id, user_or_group_id, perms) " + - "VALUES(?, ?, ?)", spaceId, userToAdd, perms); + "INSERT INTO user_roles(space_id, user_id, role_id)" + + " VALUES(?, ?, ?)", spaceId, userToAdd, role); response.status(200); return new JSONObject() .put("username", userToAdd) - .put("permissions", perms); + .put("role", role); } public static class Message { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 4dac87f..d1b834a 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -1,11 +1,10 @@ package com.manning.apisecurityinaction.controller; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Base64; import com.lambdaworks.crypto.SCryptUtil; import org.dalesbred.Database; -import org.dalesbred.query.QueryBuilder; import org.json.JSONObject; import spark.*; @@ -105,21 +104,15 @@ public Filter requirePermission(String method, String permission) { var spaceId = Long.parseLong(request.params(":spaceId")); var username = (String) request.attribute("subject"); - List groups = request.attribute("groups"); - var queryBuilder = new QueryBuilder( - "SELECT perms FROM permissions " + - "WHERE space_id = ? " + - "AND (user_or_group_id = ?", spaceId, username); + var perms = database.findOptional(String.class, + "SELECT rp.perms " + + " FROM role_permissions rp JOIN user_roles ur" + + " ON rp.role_id = ur.role_id" + + " WHERE ur.space_id = ? AND ur.user_id = ?", + spaceId, username).orElse(""); - for (var group : groups) { - queryBuilder.append(" OR user_or_group_id = ?", group); - } - queryBuilder.append(")"); - - var perms = database.findAll(String.class, - queryBuilder.build()); - if (perms.stream().noneMatch(p -> p.contains(permission))) { + if (!perms.contains(permission)) { halt(403); } }; diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 0c24898..1d9bc04 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -35,11 +35,21 @@ CREATE TABLE audit_log( ); CREATE SEQUENCE audit_id_seq; -CREATE TABLE permissions( +CREATE TABLE role_permissions( + role_id VARCHAR(30) NOT NULL PRIMARY KEY, + perms VARCHAR(3) NOT NULL +); +INSERT INTO role_permissions(role_id, perms) + VALUES ('owner', 'rwd'), + ('moderator', 'rd'), + ('member', 'rw'), + ('observer', 'r'); + +CREATE TABLE user_roles( space_id INT NOT NULL REFERENCES spaces(space_id), - user_or_group_id VARCHAR(30) NOT NULL, - perms VARCHAR(3) NOT NULL, - PRIMARY KEY (space_id, user_or_group_id) + user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), + role_id VARCHAR(30) NOT NULL REFERENCES role_permissions(role_id), + PRIMARY KEY (space_id, user_id) ); CREATE TABLE tokens( @@ -55,6 +65,7 @@ GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; GRANT DELETE ON messages TO natter_api_user; GRANT SELECT, INSERT ON users TO natter_api_user; GRANT SELECT, INSERT ON audit_log TO natter_api_user; -GRANT SELECT, INSERT ON permissions TO natter_api_user; GRANT SELECT, INSERT, DELETE ON tokens TO natter_api_user; -GRANT SELECT, INSERT, DELETE ON group_members TO natter_api_user; \ No newline at end of file +GRANT SELECT, INSERT, DELETE ON group_members TO natter_api_user; +GRANT SELECT, INSERT, DELETE ON user_roles TO natter_api_user; +GRANT SELECT ON role_permissions TO natter_api_user; \ No newline at end of file From f8e6a80bebd1014765afc1e66e3c839878a18b07 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 14 Sep 2019 22:56:13 +0100 Subject: [PATCH 130/209] Allow users to specify a role when creating a session --- .../com/manning/apisecurityinaction/Main.java | 3 +-- .../controller/SpaceController.java | 26 ++++++++++++------- .../controller/TokenController.java | 5 ++++ .../controller/UserController.java | 18 ++++++++++--- natter-api/src/main/resources/schema.sql | 2 +- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 51452d3..111447c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -74,8 +74,7 @@ public static void main(String... args) throws Exception { var clientSecret = "60ho9IS3d6/A+Zzvdn9Y4laiGnI/1TddTM95lEHjArw="; var introspectionEndpoint = URI.create("https://as.example.com:8443/oauth2/introspect"); - SecureTokenStore tokenStore = new OAuth2TokenStore( - introspectionEndpoint, clientId, clientSecret); + SecureTokenStore tokenStore = new DatabaseTokenStore(database); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index a0fcc11..ef2865f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -43,9 +43,12 @@ public JSONObject createSpace(Request request, Response response) { "INSERT INTO spaces(space_id, name, owner) " + "VALUES(?, ?, ?);", spaceId, spaceName, owner); - database.updateUnique( - "INSERT INTO user_roles(space_id, user_id, role_id) " + - "VALUES(?, ?, ?)", spaceId, owner, "owner"); + // Grant all roles to the owner + for (var role : DEFINED_ROLES) { + database.updateUnique( + "INSERT INTO user_roles(space_id, user_id, role_id) " + + "VALUES(?, ?, ?)", spaceId, owner, role); + } response.status(201); response.header("Location", "/spaces/" + spaceId); @@ -123,20 +126,25 @@ public JSONObject addMember(Request request, Response response) { var json = new JSONObject(request.body()); var spaceId = Long.parseLong(request.params(":spaceId")); var userToAdd = json.getString("username"); - var role = json.optString("role", "member"); + var roles = json.optJSONArray("roles"); + if (roles == null) { + roles = new JSONArray().put("member"); + } - if (!DEFINED_ROLES.contains(role)) { + if (!DEFINED_ROLES.containsAll(roles.toList())) { throw new IllegalArgumentException("invalid role"); } - database.updateUnique( - "INSERT INTO user_roles(space_id, user_id, role_id)" + - " VALUES(?, ?, ?)", spaceId, userToAdd, role); + for (var role : roles.toList()) { + database.updateUnique( + "INSERT INTO user_roles(space_id, user_id, role_id)" + + " VALUES(?, ?, ?)", spaceId, userToAdd, role); + } response.status(200); return new JSONObject() .put("username", userToAdd) - .put("role", role); + .put("roles", roles); } public static class Message { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java index 841b0e7..d2f7c6c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java @@ -29,6 +29,11 @@ public JSONObject login(Request request, Response response) { token.attributes.put("scope", scope); } + var role = request.queryParams("role"); + if (role != null) { + token.attributes.put("role", role); + } + var tokenId = tokenStore.create(request, token); response.status(201); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index d1b834a..96aaf54 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -5,6 +5,7 @@ import com.lambdaworks.crypto.SCryptUtil; import org.dalesbred.Database; +import org.dalesbred.query.QueryBuilder; import org.json.JSONObject; import spark.*; @@ -105,14 +106,25 @@ public Filter requirePermission(String method, String permission) { var spaceId = Long.parseLong(request.params(":spaceId")); var username = (String) request.attribute("subject"); - var perms = database.findOptional(String.class, + var query = new QueryBuilder( "SELECT rp.perms " + " FROM role_permissions rp JOIN user_roles ur" + " ON rp.role_id = ur.role_id" + " WHERE ur.space_id = ? AND ur.user_id = ?", - spaceId, username).orElse(""); + spaceId, username); - if (!perms.contains(permission)) { + // If the session is restricted to a single role + // then enforce that restriction here. + var role = (String) request.attribute("role"); + if (role != null) { + query.append(" AND ur.role_id = ?", role); + } + + // With multiple roles this could return more than one + // set of permissions + var perms = database.findAll(String.class, query.build()); + + if (perms.stream().noneMatch(p -> p.contains(permission))) { halt(403); } }; diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 1d9bc04..f05339b 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -49,8 +49,8 @@ CREATE TABLE user_roles( space_id INT NOT NULL REFERENCES spaces(space_id), user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), role_id VARCHAR(30) NOT NULL REFERENCES role_permissions(role_id), - PRIMARY KEY (space_id, user_id) ); +CREATE INDEX user_roles_idx ON user_roles(space_id, user_id); CREATE TABLE tokens( token_id VARCHAR(30) PRIMARY KEY, From 22c49dfcbc3250a06ba1071211e9d7526db659a2 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 16 Sep 2019 22:46:52 +0100 Subject: [PATCH 131/209] Implement ABAC with Drools --- natter-api/pom.xml | 18 ++++++ .../com/manning/apisecurityinaction/Main.java | 3 + .../controller/ABACAccessController.java | 57 +++++++++++++++++++ .../controller/DroolsAccessController.java | 55 ++++++++++++++++++ .../src/main/resources/META-INF/kmodule.xml | 3 + natter-api/src/main/resources/accessrules.drl | 15 +++++ 6 files changed, 151 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/DroolsAccessController.java create mode 100644 natter-api/src/main/resources/META-INF/kmodule.xml create mode 100644 natter-api/src/main/resources/accessrules.drl diff --git a/natter-api/pom.xml b/natter-api/pom.xml index 46778f7..bd4ab60 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -11,6 +11,7 @@ com.manning.apisecurityinaction.Main + 7.26.0.Final @@ -53,5 +54,22 @@ nimbus-jose-jwt 7.2.1 + + + + org.kie + kie-api + ${drools.version} + + + org.drools + drools-core + ${drools.version} + + + org.drools + drools-compiler + ${drools.version} + \ No newline at end of file diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 111447c..cdbf9c1 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -84,6 +84,9 @@ public static void main(String... args) throws Exception { before(auditController::auditRequestStart); afterAfter(auditController::auditRequestEnd); + var droolsController = new DroolsAccessController(); + before("/*", droolsController::enforcePolicy); + before("/sessions", userController::requireAuthentication); post("/sessions", tokenController::login); delete("/sessions", tokenController::logout); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java new file mode 100644 index 0000000..6ee0976 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java @@ -0,0 +1,57 @@ +package com.manning.apisecurityinaction.controller; + +import java.time.LocalTime; +import java.util.*; + +import spark.*; + +import static spark.Spark.halt; + +public abstract class ABACAccessController { + + public void enforcePolicy(Request request, Response response) { + + var subjectAttrs = new HashMap(); + subjectAttrs.put("user", request.attribute("subject")); + subjectAttrs.put("groups", request.attribute("groups")); + + var resourceAttrs = new HashMap(); + resourceAttrs.put("path", request.pathInfo()); + resourceAttrs.put("space", request.params(":spaceId")); + + var actionAttrs = new HashMap(); + actionAttrs.put("method", request.requestMethod()); + + var envAttrs = new HashMap(); + envAttrs.put("timeOfDay", LocalTime.now().withHour(23)); + envAttrs.put("ip", request.ip()); + + var permitted = checkPermitted(subjectAttrs, resourceAttrs, + actionAttrs, envAttrs); + + if (!permitted) { + halt(403); + } + } + + abstract boolean checkPermitted( + Map subject, + Map resource, + Map action, + Map env); + + public static class Decision { + private boolean permit = true; + + public void deny() { + permit = false; + } + + public void permit() { + } + + boolean isPermitted() { + return permit; + } + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/DroolsAccessController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/DroolsAccessController.java new file mode 100644 index 0000000..aba92bd --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/DroolsAccessController.java @@ -0,0 +1,55 @@ +package com.manning.apisecurityinaction.controller; + +import java.util.*; + +import org.kie.api.KieServices; +import org.kie.api.runtime.KieContainer; + +public class DroolsAccessController extends ABACAccessController { + + private final KieContainer kieContainer; + + public DroolsAccessController() { + this.kieContainer = KieServices.get().getKieClasspathContainer(); + } + + @Override + boolean checkPermitted(Map subject, + Map resource, + Map action, + Map env) { + + var session = kieContainer.newKieSession(); + try { + var decision = new Decision(); + session.setGlobal("decision", decision); + + session.insert(new Subject(subject)); + session.insert(new Resource(resource)); + session.insert(new Action(action)); + session.insert(new Environment(env)); + + session.fireAllRules(); + return decision.isPermitted(); + + } finally { + session.dispose(); + } + } + + public static class Subject extends HashMap { + Subject(Map m) { super(m); } + } + + public static class Resource extends HashMap { + Resource(Map m) { super(m); } + } + + public static class Action extends HashMap { + Action(Map m) { super(m); } + } + + public static class Environment extends HashMap { + Environment(Map m) { super(m); } + } +} diff --git a/natter-api/src/main/resources/META-INF/kmodule.xml b/natter-api/src/main/resources/META-INF/kmodule.xml new file mode 100644 index 0000000..ebb4ba4 --- /dev/null +++ b/natter-api/src/main/resources/META-INF/kmodule.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/natter-api/src/main/resources/accessrules.drl b/natter-api/src/main/resources/accessrules.drl new file mode 100644 index 0000000..efbb5db --- /dev/null +++ b/natter-api/src/main/resources/accessrules.drl @@ -0,0 +1,15 @@ +package com.manning.apisecurityinaction.rules; + +import com.manning.apisecurityinaction.controller.DroolsAccessController.*; +import com.manning.apisecurityinaction.controller.ABACAccessController.Decision; + +global Decision decision; + +rule "deny moderation outside office hours" + when + Action( this["method"] == "DELETE" ) + Environment( this["timeOfDay"].hour < 9 + || this["timeOfDay"].hour > 17 ) + then + decision.deny(); +end \ No newline at end of file From 717d3cebf523d80041637315d44860c632886fc5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 5 Oct 2019 09:45:41 +0100 Subject: [PATCH 132/209] Update README for chapters 8 and 9 --- .gitignore | 1 + README.md | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/.gitignore b/.gitignore index 058db06..080f882 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.iml target/ *.zip +.DS_Store diff --git a/README.md b/README.md index 15bdfe9..cf69422 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,13 @@ of the next chapter. - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter07) - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter07-end) + +### Chapter 8 - Identity-based access control + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter08) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter08-end) + +### Chapter 9 - Capability security and Macaroons + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter09) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter09-end) From 0624d99b2db459bef1f6a38e24623f3c2a52800e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 5 Oct 2019 10:33:04 +0100 Subject: [PATCH 133/209] Update README to point to MEAP --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cf69422..485f7f3 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ This repository contains source code to accompany the upcoming book API Security in Action, written by Neil Madden and to be published by Manning Publications some time next year. If you have stumbled across this repository by accident, it is unlikely to make much sense on its -own at this stage. Please see *TBC* for early access. +own at this stage. Please see [Manning's website](https://www.manning.com/books/api-security-in-action?a_aid=api_security_in_action&a_bid=6806e3b6) +for early access. The git repo is organized with a separate branch for each chapter, starting with Chapter 2. Actually there are two (or more) branches @@ -15,6 +16,28 @@ final source code after all the alterations in that chapter. Typically the source code at the end of a chapter is also identical to the start of the next chapter. +The source code can also be downloaded as a zip file from the early +access website. + +## Prerequisites + +The following are needed to run the code examples: + + - Java 11 or later. See https://adoptopenjdk.net for installers. + - A recent version of [Apache Maven](https://maven.apache.org) - I use 3.6.1. + - For testing, [curl](https://curl.haxx.se). On Mac OS X you should install + a version of curl linked against OpenSSL rather than Secure Transport, otherwise + you may need to adjust the examples in the book. + - I highly recommend installing [mkcert](https://github.com/FiloSottile/mkcert) + for working with SSL certificates from chapter 3 onwards. + +The API server for each chapter can be started using the command + + mvn clean compile exec:java + +This will start the Spark/Jetty server running on port 4567. See chapter +descriptions for HTTP requests that can be + ## Chapters ### Chapter 2 - Secure API development From 8f13b7df0a219993e83d244d9f3d9d65fdab10b7 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 8 Oct 2019 22:26:32 +0100 Subject: [PATCH 134/209] Split out lookup of permissions from enforcement --- .../com/manning/apisecurityinaction/Main.java | 4 ++ .../controller/ABACAccessController.java | 2 +- .../controller/UserController.java | 47 ++++++++++--------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index cdbf9c1..ba2bb31 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -100,6 +100,10 @@ public static void main(String... args) throws Exception { tokenController.requireScope("POST", "create_space")); post("/spaces", spaceController::createSpace); + before("/spaces/:spaceId/messages", userController::lookupPermissions); + before("/spaces/:spaceId/messages/*", userController::lookupPermissions); + before("/spaces/:spaceId/members", userController::lookupPermissions); + before("/spaces/*/messages", tokenController.requireScope("POST", "post_message")); before("/spaces/:spaceId/messages", diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java index 6ee0976..5e0a4ad 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java @@ -23,7 +23,7 @@ public void enforcePolicy(Request request, Response response) { actionAttrs.put("method", request.requestMethod()); var envAttrs = new HashMap(); - envAttrs.put("timeOfDay", LocalTime.now().withHour(23)); + envAttrs.put("timeOfDay", LocalTime.now()); envAttrs.put("ip", request.ip()); var permitted = checkPermitted(subjectAttrs, resourceAttrs, diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 96aaf54..ff95684 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -95,6 +95,29 @@ public void requireAuthentication(Request request, Response response) { } } + public void lookupPermissions(Request request, Response response) { + var spaceId = Long.parseLong(request.params(":spaceId")); + var username = (String) request.attribute("subject"); + + if (username == null) return; + + var query = new QueryBuilder( + "SELECT rp.perms " + + " FROM role_permissions rp JOIN user_roles ur" + + " ON rp.role_id = ur.role_id" + + " WHERE ur.space_id = ? AND ur.user_id = ?", + spaceId, username); + + var role = (String) request.attribute("role"); + if (role != null) { + query.append(" AND ur.role_id = ?", role); + } + + var perms = String.join("", + database.findAll(String.class, query.build())); + request.attribute("perms", perms); + } + public Filter requirePermission(String method, String permission) { return (request, response) -> { if (!method.equals(request.requestMethod())) { @@ -103,28 +126,8 @@ public Filter requirePermission(String method, String permission) { requireAuthentication(request, response); - var spaceId = Long.parseLong(request.params(":spaceId")); - var username = (String) request.attribute("subject"); - - var query = new QueryBuilder( - "SELECT rp.perms " + - " FROM role_permissions rp JOIN user_roles ur" + - " ON rp.role_id = ur.role_id" + - " WHERE ur.space_id = ? AND ur.user_id = ?", - spaceId, username); - - // If the session is restricted to a single role - // then enforce that restriction here. - var role = (String) request.attribute("role"); - if (role != null) { - query.append(" AND ur.role_id = ?", role); - } - - // With multiple roles this could return more than one - // set of permissions - var perms = database.findAll(String.class, query.build()); - - if (perms.stream().noneMatch(p -> p.contains(permission))) { + var perms = request.attribute("perms"); + if (!perms.contains(permission)) { halt(403); } }; From 324588e8853b48efe30fe055197376f39a5ac8f8 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 9 Oct 2019 21:23:48 +0100 Subject: [PATCH 135/209] Initial capability URI implementation --- .../com/manning/apisecurityinaction/Main.java | 26 ++++++----- .../controller/CapabilityController.java | 43 +++++++++++++++++++ .../controller/SpaceController.java | 19 ++++---- natter-api/src/main/resources/schema.sql | 2 +- 4 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index ba2bb31..8b7f7b8 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -37,8 +37,19 @@ public static void main(String... args) throws Exception { datasource = JdbcConnectionPool.create( "jdbc:h2:mem:natter", "natter_api_user", "password"); + + var keyPassword = System.getProperty("keystore.password", + "changeit").toCharArray(); + var keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new FileInputStream("keystore.p12"), + keyPassword); + var macKey = keyStore.getKey("hmac-key", keyPassword); + var encKey = keyStore.getKey("aes-key", keyPassword); + var database = Database.forDataSource(datasource); - var spaceController = new SpaceController(database); + var capController = new CapabilityController( + new DatabaseTokenStore(database)); + var spaceController = new SpaceController(database, capController); var userController = new UserController(database); var rateLimiter = RateLimiter.create(2.0d); @@ -58,13 +69,6 @@ public static void main(String... args) throws Exception { } })); - var keyPassword = System.getProperty("keystore.password", - "changeit").toCharArray(); - var keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(new FileInputStream("keystore.p12"), - keyPassword); - var macKey = keyStore.getKey("hmac-key", keyPassword); - var encKey = keyStore.getKey("aes-key", keyPassword); var header = new JSONObject() .put("alg", "HS256") @@ -100,9 +104,9 @@ public static void main(String... args) throws Exception { tokenController.requireScope("POST", "create_space")); post("/spaces", spaceController::createSpace); - before("/spaces/:spaceId/messages", userController::lookupPermissions); - before("/spaces/:spaceId/messages/*", userController::lookupPermissions); - before("/spaces/:spaceId/members", userController::lookupPermissions); + before("/spaces/:spaceId/messages", capController::lookupPermissions); + before("/spaces/:spaceId/messages/*", capController::lookupPermissions); + before("/spaces/:spaceId/members", capController::lookupPermissions); before("/spaces/*/messages", tokenController.requireScope("POST", "post_message")); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java new file mode 100644 index 0000000..be02f0d --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java @@ -0,0 +1,43 @@ +package com.manning.apisecurityinaction.controller; + +import java.net.URI; +import java.time.Instant; +import java.util.Objects; + +import com.manning.apisecurityinaction.token.SecureTokenStore; +import com.manning.apisecurityinaction.token.TokenStore.Token; +import spark.*; + +public class CapabilityController { + + private final SecureTokenStore tokenStore; + + public CapabilityController(SecureTokenStore tokenStore) { + this.tokenStore = tokenStore; + } + + public URI createUri(Request request, String path, String perms) { + + var token = new Token(Instant.MAX, null); + token.attributes.put("path", path); + token.attributes.put("perms", perms); + + var tokenId = tokenStore.create(request, token); + + var uri = URI.create(request.url()); + return uri.resolve(path + "?access_token=" + tokenId); + } + + public void lookupPermissions(Request request, Response response) { + var tokenId = request.queryParams("access_token"); + if (tokenId == null) return; + + tokenStore.read(request, tokenId).ifPresent(token -> { + var tokenPath = token.attributes.get("path"); + if (Objects.equals(tokenPath, request.pathInfo())) { + request.attribute("perms", + token.attributes.get("perms")); + } + }); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index ef2865f..fab3ff5 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -14,9 +14,12 @@ public class SpaceController { Set.of("owner", "moderator", "member", "observer"); private final Database database; + private final CapabilityController capabilityController; - public SpaceController(Database database) { + public SpaceController(Database database, + CapabilityController capabilityController) { this.database = database; + this.capabilityController = capabilityController; } public JSONObject createSpace(Request request, Response response) { @@ -43,19 +46,15 @@ public JSONObject createSpace(Request request, Response response) { "INSERT INTO spaces(space_id, name, owner) " + "VALUES(?, ?, ?);", spaceId, spaceName, owner); - // Grant all roles to the owner - for (var role : DEFINED_ROLES) { - database.updateUnique( - "INSERT INTO user_roles(space_id, user_id, role_id) " + - "VALUES(?, ?, ?)", spaceId, owner, role); - } + var uri = capabilityController.createUri(request, + "/spaces/" + spaceId, "rwd"); response.status(201); - response.header("Location", "/spaces/" + spaceId); + response.header("Location", uri.toASCIIString()); return new JSONObject() - .put("name", spaceName) .put("uri", "/spaces/" + spaceId); - + .put("name", spaceName) + .put("uri", uri); }); } diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index f05339b..57e7114 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -54,7 +54,7 @@ CREATE INDEX user_roles_idx ON user_roles(space_id, user_id); CREATE TABLE tokens( token_id VARCHAR(30) PRIMARY KEY, - user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), + user_id VARCHAR(30) REFERENCES users(user_id), expiry TIMESTAMP NOT NULL, attributes VARCHAR(4096) NOT NULL ); From 3905e4a7cbe019aafe4a2651bc2ad1b19dbc19ed Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 9 Oct 2019 22:11:27 +0100 Subject: [PATCH 136/209] Fix NPE bug in permission check --- .../manning/apisecurityinaction/controller/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index ff95684..0cf9d68 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -127,7 +127,7 @@ public Filter requirePermission(String method, String permission) { requireAuthentication(request, response); var perms = request.attribute("perms"); - if (!perms.contains(permission)) { + if (perms == null || !perms.contains(permission)) { halt(403); } }; From e75a1e7be93c49e72dcd54dc6c23f70343816f34 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 9 Oct 2019 22:15:24 +0100 Subject: [PATCH 137/209] Rearrange authentication in UserController --- .../apisecurityinaction/controller/UserController.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 0cf9d68..47ba782 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -96,11 +96,11 @@ public void requireAuthentication(Request request, Response response) { } public void lookupPermissions(Request request, Response response) { + requireAuthentication(request, response); + var spaceId = Long.parseLong(request.params(":spaceId")); var username = (String) request.attribute("subject"); - if (username == null) return; - var query = new QueryBuilder( "SELECT rp.perms " + " FROM role_permissions rp JOIN user_roles ur" + @@ -124,8 +124,6 @@ public Filter requirePermission(String method, String permission) { return; } - requireAuthentication(request, response); - var perms = request.attribute("perms"); if (perms == null || !perms.contains(permission)) { halt(403); From 62a4991c01db1affabedd67b6b422673f0edff0a Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 9 Oct 2019 23:20:41 +0100 Subject: [PATCH 138/209] Flesh out capabilities for other API methods. --- .../controller/SpaceController.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index fab3ff5..1a9e937 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -48,13 +48,22 @@ public JSONObject createSpace(Request request, Response response) { var uri = capabilityController.createUri(request, "/spaces/" + spaceId, "rwd"); + var messagesUri = capabilityController.createUri(request, + "/spaces/" + spaceId + "/messages", "rwd"); + var messagesReadWriteUri = capabilityController.createUri( + request, "/spaces/" + spaceId + "/messages", "rw"); + var messagesReadOnlyUri = capabilityController.createUri( + request, "/spaces/" + spaceId + "/messages", "r"); response.status(201); response.header("Location", uri.toASCIIString()); return new JSONObject() .put("name", spaceName) - .put("uri", uri); + .put("uri", uri) + .put("messages-rwd", messagesUri) + .put("messages-rw", messagesReadWriteUri) + .put("messages-r", messagesReadOnlyUri); }); } @@ -84,9 +93,15 @@ public JSONObject postMessage(Request request, Response response) { spaceId, msgId, user, message); response.status(201); - var uri = "/spaces/" + spaceId + "/messages/" + msgId; - response.header("Location", uri); - return new JSONObject().put("uri", uri); + var uri = capabilityController.createUri(request, + "/spaces/" + spaceId + "/messages/" + msgId, "rd"); + var readOnlyUri = capabilityController.createUri(request, + "/spaces/" + spaceId + "/messages/" + msgId, "r"); + + response.header("Location", uri.toASCIIString()); + return new JSONObject() + .put("uri", uri) + .put("read-only", readOnlyUri); }); } @@ -115,9 +130,12 @@ public JSONArray findMessages(Request request, Response response) { "WHERE space_id = ? AND msg_time >= ?;", spaceId, since); + var perms = request.attribute("perms") + .replace("w", ""); response.status(200); return new JSONArray(messages.stream() .map(msgId -> "/spaces/" + spaceId + "/messages/" + msgId) + .map(path -> capabilityController.createUri(request, path, perms)) .collect(Collectors.toList())); } From 663fd1f8b5af4a03c8064c9f217860a0aad9030a Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 10 Oct 2019 14:47:27 +0100 Subject: [PATCH 139/209] Switch to stateless capability tokens --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 2 +- .../com/manning/apisecurityinaction/token/JsonTokenStore.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 8b7f7b8..c396128 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -48,7 +48,7 @@ public static void main(String... args) throws Exception { var database = Database.forDataSource(datasource); var capController = new CapabilityController( - new DatabaseTokenStore(database)); + new EncryptedTokenStore(new JsonTokenStore(), encKey)); var spaceController = new SpaceController(database, capController); var userController = new UserController(database); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java index 41382dd..224688c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java @@ -29,7 +29,7 @@ public Optional read(Request request, String tokenId) { var decoded = Base64.getUrlDecoder().decode(tokenId); var json = new JSONObject(new String(decoded, UTF_8)); var expiry = Instant.ofEpochSecond(json.getInt("exp")); - var username = json.getString("sub"); + var username = json.optString("sub"); var audience = json.getJSONArray("aud").toList(); var attrs = json.getJSONObject("attrs"); From ef77e202dc5d9caad06d5018e9f99eda3513dc20 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 12 Oct 2019 22:11:24 +0100 Subject: [PATCH 140/209] Add a simple capability-based message browser --- .../com/manning/apisecurityinaction/Main.java | 2 +- .../controller/CapabilityController.java | 12 ++++--- .../src/main/resources/public/capability.html | 27 ++++++++++++++++ .../src/main/resources/public/capability.js | 31 +++++++++++++++++++ 4 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 natter-api/src/main/resources/public/capability.html create mode 100644 natter-api/src/main/resources/public/capability.js diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index c396128..8aa09bc 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -48,7 +48,7 @@ public static void main(String... args) throws Exception { var database = Database.forDataSource(datasource); var capController = new CapabilityController( - new EncryptedTokenStore(new JsonTokenStore(), encKey)); + new HmacTokenStore(new JsonTokenStore(), macKey)); var spaceController = new SpaceController(database, capController); var userController = new UserController(database); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java index be02f0d..7e82dab 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java @@ -1,6 +1,6 @@ package com.manning.apisecurityinaction.controller; -import java.net.URI; +import java.net.*; import java.time.Instant; import java.util.Objects; @@ -17,15 +17,19 @@ public CapabilityController(SecureTokenStore tokenStore) { } public URI createUri(Request request, String path, String perms) { - var token = new Token(Instant.MAX, null); token.attributes.put("path", path); token.attributes.put("perms", perms); var tokenId = tokenStore.create(request, token); - var uri = URI.create(request.url()); - return uri.resolve(path + "?access_token=" + tokenId); + var base = URI.create(request.url()); + try { + return new URI(base.getScheme(), tokenId, base.getHost(), + base.getPort(), path, null, null); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } } public void lookupPermissions(Request request, Response response) { diff --git a/natter-api/src/main/resources/public/capability.html b/natter-api/src/main/resources/public/capability.html new file mode 100644 index 0000000..bafc1b5 --- /dev/null +++ b/natter-api/src/main/resources/public/capability.html @@ -0,0 +1,27 @@ + + + Capability test page + + + + + +

Natter

+ + Load messages + + + +
AuthorTimeMessage
+ + \ No newline at end of file diff --git a/natter-api/src/main/resources/public/capability.js b/natter-api/src/main/resources/public/capability.js new file mode 100644 index 0000000..e616c03 --- /dev/null +++ b/natter-api/src/main/resources/public/capability.js @@ -0,0 +1,31 @@ +function getCap(url, callback) { + let capUrl = new URL(url); + let token = capUrl.username; + capUrl.username = ''; + capUrl.search = '?access_token=' + encodeURIComponent(token); + + return fetch(capUrl.href) + .then(response => response.json()) + .then(callback) + .catch(err => console.error('Error: ', err)); +} +function loadMessages(link) { + getCap(link.href, async messages => { + for (let messageUrl of messages) { + await loadMessage(messageUrl); + } + }); + return false; +} +function loadMessage(capUrl) { + return getCap(capUrl, message => { + let table = document.getElementById('messages'); + let row = table.appendChild(document.createElement('tr')); + row.appendChild(document.createElement('td')) + .textContent = message.author; + row.appendChild(document.createElement('td')) + .textContent = message.time; + row.appendChild(document.createElement('td')) + .textContent = message.message; + }); +} From c1519692452ee7e474561079d8331c3224308c5e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 12 Oct 2019 22:14:06 +0100 Subject: [PATCH 141/209] Fix stray quote --- natter-api/src/main/resources/public/capability.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/resources/public/capability.html b/natter-api/src/main/resources/public/capability.html index bafc1b5..6c42088 100644 --- a/natter-api/src/main/resources/public/capability.html +++ b/natter-api/src/main/resources/public/capability.html @@ -16,7 +16,7 @@

Natter

- Load messages From 00be059c10ce1223212e2340c764d4336bfb29c7 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 12 Oct 2019 22:58:05 +0100 Subject: [PATCH 142/209] Use Authorization header for capability tokens --- .../controller/CapabilityController.java | 6 ++++-- natter-api/src/main/resources/public/capability.js | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java index 7e82dab..5a0a2c3 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java @@ -33,8 +33,10 @@ public URI createUri(Request request, String path, String perms) { } public void lookupPermissions(Request request, Response response) { - var tokenId = request.queryParams("access_token"); - if (tokenId == null) return; + var authHeader = request.headers("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) + return; + var tokenId = authHeader.substring(7).trim(); tokenStore.read(request, tokenId).ifPresent(token -> { var tokenPath = token.attributes.get("path"); diff --git a/natter-api/src/main/resources/public/capability.js b/natter-api/src/main/resources/public/capability.js index e616c03..1102ad9 100644 --- a/natter-api/src/main/resources/public/capability.js +++ b/natter-api/src/main/resources/public/capability.js @@ -2,9 +2,10 @@ function getCap(url, callback) { let capUrl = new URL(url); let token = capUrl.username; capUrl.username = ''; - capUrl.search = '?access_token=' + encodeURIComponent(token); - return fetch(capUrl.href) + return fetch(capUrl.href, { + headers: { 'Authorization': 'Bearer ' + token } + }) .then(response => response.json()) .then(callback) .catch(err => console.error('Error: ', err)); From 8dcaebd1c286c4373dfcfb3d7617e1ade5486cc2 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 12 Oct 2019 23:07:20 +0100 Subject: [PATCH 143/209] Remove test code --- natter-api/src/main/resources/public/capability.html | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/natter-api/src/main/resources/public/capability.html b/natter-api/src/main/resources/public/capability.html index 6c42088..629fea2 100644 --- a/natter-api/src/main/resources/public/capability.html +++ b/natter-api/src/main/resources/public/capability.html @@ -3,16 +3,6 @@ Capability test page -

Natter

From 7bfc5f8428fc76a8e0ae4e685e1e561206562e9d Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 13 Oct 2019 14:16:43 +0100 Subject: [PATCH 144/209] Use cookies for authN, capabilities for authZ --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 2 +- .../apisecurityinaction/controller/TokenController.java | 5 ++--- natter-api/src/main/resources/public/capability.js | 5 ++++- natter-api/src/main/resources/public/login.js | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 8aa09bc..63c7c67 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -78,7 +78,7 @@ public static void main(String... args) throws Exception { var clientSecret = "60ho9IS3d6/A+Zzvdn9Y4laiGnI/1TddTM95lEHjArw="; var introspectionEndpoint = URI.create("https://as.example.com:8443/oauth2/introspect"); - SecureTokenStore tokenStore = new DatabaseTokenStore(database); + SecureTokenStore tokenStore = new CookieTokenStore(); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java index d2f7c6c..e785ac4 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java @@ -42,11 +42,10 @@ public JSONObject login(Request request, Response response) { } public void validateToken(Request request, Response response) { - var tokenId = request.headers("Authorization"); - if (tokenId == null || !tokenId.startsWith("Bearer ")) { + var tokenId = request.headers("X-CSRF-Token"); + if (tokenId == null) { return; } - tokenId = tokenId.substring(7); tokenStore.read(request, tokenId).ifPresent(token -> { if (Instant.now().isBefore(token.expiry)) { diff --git a/natter-api/src/main/resources/public/capability.js b/natter-api/src/main/resources/public/capability.js index 1102ad9..3895274 100644 --- a/natter-api/src/main/resources/public/capability.js +++ b/natter-api/src/main/resources/public/capability.js @@ -4,7 +4,10 @@ function getCap(url, callback) { capUrl.username = ''; return fetch(capUrl.href, { - headers: { 'Authorization': 'Bearer ' + token } + headers: { + 'Authorization': 'Bearer ' + token, + 'X-CSRF-Token': localStorage.getItem('token') + } }) .then(response => response.json()) .then(callback) diff --git a/natter-api/src/main/resources/public/login.js b/natter-api/src/main/resources/public/login.js index 8583bb7..021aca6 100644 --- a/natter-api/src/main/resources/public/login.js +++ b/natter-api/src/main/resources/public/login.js @@ -14,7 +14,7 @@ function login(username, password) { if (res.ok) { res.json().then(json => { localStorage.setItem('token', json.token); - window.location.replace('/natter.html'); + window.location.replace('/capability.html'); }); } }) From 3e594da7e11ec9c46e0aced9d281929a39a124e8 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 14 Oct 2019 16:29:16 +0100 Subject: [PATCH 145/209] Implement MacaroonTokenStore --- natter-api/pom.xml | 5 ++ .../com/manning/apisecurityinaction/Main.java | 5 +- .../token/MacaroonTokenStore.java | 78 +++++++++++++++++++ .../src/main/resources/public/capability.html | 4 +- 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/MacaroonTokenStore.java diff --git a/natter-api/pom.xml b/natter-api/pom.xml index bd4ab60..33deb57 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -54,6 +54,11 @@ nimbus-jose-jwt 7.2.1
+ + com.github.nitram509 + jmacaroons + 0.4.1 + diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 63c7c67..6dfeaed 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -36,7 +36,7 @@ public static void main(String... args) throws Exception { createTables(datasource.getConnection()); datasource = JdbcConnectionPool.create( "jdbc:h2:mem:natter", "natter_api_user", "password"); - + var database = Database.forDataSource(datasource); var keyPassword = System.getProperty("keystore.password", "changeit").toCharArray(); @@ -46,9 +46,8 @@ public static void main(String... args) throws Exception { var macKey = keyStore.getKey("hmac-key", keyPassword); var encKey = keyStore.getKey("aes-key", keyPassword); - var database = Database.forDataSource(datasource); var capController = new CapabilityController( - new HmacTokenStore(new JsonTokenStore(), macKey)); + new MacaroonTokenStore(new JsonTokenStore(), macKey)); var spaceController = new SpaceController(database, capController); var userController = new UserController(database); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/MacaroonTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/MacaroonTokenStore.java new file mode 100644 index 0000000..c7023ee --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/MacaroonTokenStore.java @@ -0,0 +1,78 @@ +package com.manning.apisecurityinaction.token; + +import java.security.Key; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; + +import com.github.nitram509.jmacaroons.*; +import com.github.nitram509.jmacaroons.verifier.TimestampCaveatVerifier; +import spark.Request; + +public class MacaroonTokenStore implements SecureTokenStore { + private final TokenStore delegate; + private final Key macKey; + + public MacaroonTokenStore(TokenStore delegate, Key macKey) { + this.delegate = delegate; + this.macKey = macKey; + } + + @Override + public String create(Request request, Token token) { + var identifier = delegate.create(request, token); + var macaroon = MacaroonsBuilder.create("", + macKey.getEncoded(), identifier); + + if (token.expiry != Instant.MAX) { + macaroon = MacaroonsBuilder.modify(macaroon) + .add_first_party_caveat("time < " + token.expiry) + .getMacaroon(); + } + + return macaroon.serialize(); + } + + @Override + public Optional read(Request request, String tokenId) { + var macaroon = MacaroonsBuilder.deserialize(tokenId); + + var verifier = new MacaroonsVerifier(macaroon); + verifier.satisfyGeneral(new TimestampCaveatVerifier()); + verifier.satisfyExact("method = " + request.requestMethod()); + verifier.satisfyGeneral(new SinceVerifier(request)); + + if (verifier.isValid(macKey.getEncoded())) { + return delegate.read(request, macaroon.identifier); + } + return Optional.empty(); + } + + @Override + public void revoke(Request request, String tokenId) { + var macaroon = MacaroonsBuilder.deserialize(tokenId); + delegate.revoke(request, macaroon.identifier); + } + + private static class SinceVerifier implements GeneralCaveatVerifier { + private final Request request; + + private SinceVerifier(Request request) { + this.request = request; + } + + @Override + public boolean verifyCaveat(String caveat) { + if (caveat.startsWith("since > ")) { + var minSince = Instant.parse(caveat.substring(8)); + var reqSince = Instant.now().minus(1, ChronoUnit.DAYS); + if (request.queryParams("since") != null) { + reqSince = Instant.parse(request.queryParams("since")); + } + return reqSince.isAfter(minSince); + } + + return false; + } + } +} diff --git a/natter-api/src/main/resources/public/capability.html b/natter-api/src/main/resources/public/capability.html index 629fea2..c4fd1f7 100644 --- a/natter-api/src/main/resources/public/capability.html +++ b/natter-api/src/main/resources/public/capability.html @@ -6,8 +6,8 @@

Natter

- + Load messages From 11f7149c8dc1f1b21d3914d943ef3f6b3cbdb3c8 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 17 Oct 2019 14:42:48 +0100 Subject: [PATCH 146/209] Only require authentication when looking up permissions. This change anticipates chapter 9 where we will replace the lookupPermissions method with one not based on the user identity, while leaving all the requirePermission calls unchanged. --- .../apisecurityinaction/controller/UserController.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index ff95684..02cb87c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -96,11 +96,10 @@ public void requireAuthentication(Request request, Response response) { } public void lookupPermissions(Request request, Response response) { + requireAuthentication(request, response); var spaceId = Long.parseLong(request.params(":spaceId")); var username = (String) request.attribute("subject"); - if (username == null) return; - var query = new QueryBuilder( "SELECT rp.perms " + " FROM role_permissions rp JOIN user_roles ur" + @@ -124,8 +123,6 @@ public Filter requirePermission(String method, String permission) { return; } - requireAuthentication(request, response); - var perms = request.attribute("perms"); if (!perms.contains(permission)) { halt(403); From 8ca19452689487b683d0e1dfa4bd68997c3f30f2 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 4 Nov 2019 16:16:03 +0000 Subject: [PATCH 147/209] Fix schema loading code to also work within Docker --- .../main/java/com/manning/apisecurityinaction/Main.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 6dfeaed..24ff8e6 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -2,7 +2,6 @@ import java.io.FileInputStream; import java.net.URI; -import java.nio.file.*; import java.security.KeyStore; import java.sql.Connection; import java.util.Set; @@ -18,6 +17,7 @@ import spark.embeddedserver.EmbeddedServers; import spark.embeddedserver.jetty.EmbeddedJettyFactory; +import static java.nio.charset.StandardCharsets.UTF_8; import static spark.Service.SPARK_DEFAULT_PORT; import static spark.Spark.*; @@ -172,11 +172,10 @@ private static void badRequest(Exception ex, private static void createTables(Connection connection) throws Exception { try (var conn = connection; - var stmt = conn.createStatement()) { + var stmt = conn.createStatement(); + var in = Main.class.getResourceAsStream("/schema.sql")) { conn.setAutoCommit(false); - Path path = Paths.get( - Main.class.getResource("/schema.sql").toURI()); - stmt.execute(Files.readString(path)); + stmt.execute(new String(in.readAllBytes(), UTF_8)); conn.commit(); } } From 59e33b2b5b6b38e12faadf9db0efcb3c46fad9f3 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 4 Nov 2019 17:08:49 +0000 Subject: [PATCH 148/209] Build basic Docker image with Google JIB --- natter-api/natter.yaml | 13 ++++++++++++ natter-api/pom.xml | 28 ++++++++++++++++++++++++++ natter-api/src/main/jib/keystore.p12 | Bin 0 -> 755 bytes natter-api/src/main/jib/localhost.p12 | Bin 0 -> 3975 bytes 4 files changed, 41 insertions(+) create mode 100644 natter-api/natter.yaml create mode 100644 natter-api/src/main/jib/keystore.p12 create mode 100644 natter-api/src/main/jib/localhost.p12 diff --git a/natter-api/natter.yaml b/natter-api/natter.yaml new file mode 100644 index 0000000..28700c4 --- /dev/null +++ b/natter-api/natter.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + name: natter-api + labels: + app: natter +spec: + containers: + - name: natter-api + image: apisecurityinaction/natter-api + imagePullPolicy: Never + ports: + - containerPort: 4567 \ No newline at end of file diff --git a/natter-api/pom.xml b/natter-api/pom.xml index 33deb57..9a50df9 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -77,4 +77,32 @@ ${drools.version} + + + + + com.google.cloud.tools + jib-maven-plugin + 1.7.0 + + + apisecurityinaction/natter-api + + + gcr.io/distroless/java:11 + + + ${exec.mainClass} + + -Djava.security.egd=file:/dev/urandom + + + 4567 + + 1000:1000 + + + + + \ No newline at end of file diff --git a/natter-api/src/main/jib/keystore.p12 b/natter-api/src/main/jib/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..9007e6c11b68583815fceb513feab72452eed153 GIT binary patch literal 755 zcmXqLVtUWS$ZXKWw1SOOtIebBJ1-+U;0&a13u_6aG9e-C<__kgM3O2M8; zCT51i0W21FJ07k7#=#mCzP`_tsUzjX$389Ryw(Rl9-o&uJU9ICp83Vs_VPs7?X}vG zw&zhu)Pz=@s5QQ;(heB97|6q&$thweBqzX-!H~<4$dJsS%aF~G%1~*bh)^JED8eEX zl9`*TU}$P;VP<7sm57u8%)D%}v?&+qy&Nr)^IUiJj@8==WaY*v!z>($c`rK+%Ac zja8eEnMsP3fkk9}?c<*8M9&`uC0d&26i(}H_$b-LB2w4MRkB?3?XwqAKTmFyRh9iU MH#m!lnXzpF015*182|tP literal 0 HcmV?d00001 diff --git a/natter-api/src/main/jib/localhost.p12 b/natter-api/src/main/jib/localhost.p12 new file mode 100644 index 0000000000000000000000000000000000000000..3a8322f5305cbb79f7aed716437ebc6451479ede GIT binary patch literal 3975 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 literal 0 HcmV?d00001 From cceb7a54ff2d13312936620571ff9f816e74951d Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 5 Nov 2019 11:42:48 +0000 Subject: [PATCH 149/209] Remove FK constraint from tokens to users --- natter-api/src/main/resources/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 57e7114..b6de8e3 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -54,7 +54,7 @@ CREATE INDEX user_roles_idx ON user_roles(space_id, user_id); CREATE TABLE tokens( token_id VARCHAR(30) PRIMARY KEY, - user_id VARCHAR(30) REFERENCES users(user_id), + user_id VARCHAR(30), expiry TIMESTAMP NOT NULL, attributes VARCHAR(4096) NOT NULL ); From da0a2da2dbe394124ead4e8a5f628b5930c324c1 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 5 Nov 2019 20:53:50 +0000 Subject: [PATCH 150/209] Add Dockerfile for H2 database --- natter-api/docker/h2/Dockerfile | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 natter-api/docker/h2/Dockerfile diff --git a/natter-api/docker/h2/Dockerfile b/natter-api/docker/h2/Dockerfile new file mode 100644 index 0000000..5a6b9d9 --- /dev/null +++ b/natter-api/docker/h2/Dockerfile @@ -0,0 +1,22 @@ +FROM curlimages/curl:7.66.0 AS build-env + +ENV RELEASE h2-2019-10-14.zip +ENV SHA256 a72f319f1b5347a6ee9eba42718e69e2ae41e2f846b3475f9292f1e3beb59b01 + +WORKDIR /tmp + +RUN echo "$SHA256 $RELEASE" > $RELEASE.sha256 && \ + curl -sSL https://www.h2database.com/$RELEASE -o $RELEASE && \ + sha256sum -b -c $RELEASE.sha256 && \ + unzip $RELEASE && rm -f $RELEASE + +FROM gcr.io/distroless/java:11 +WORKDIR /opt +COPY --from=build-env /tmp/h2/bin /opt/h2 + +USER 1000:1000 + +EXPOSE 9092 +ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/urandom", \ + "-cp", "/opt/h2/h2-1.4.200.jar", \ + "org.h2.tools.Server", "-tcp", "-tcpAllowOthers"] \ No newline at end of file From 0ac1d620262e4028aa1fdf6ebe3380c3b8400325 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 5 Nov 2019 20:54:17 +0000 Subject: [PATCH 151/209] Disable TLS and make JDBC URL configurable --- .../main/java/com/manning/apisecurityinaction/Main.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 24ff8e6..b3e9e31 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -27,15 +27,17 @@ public static void main(String... args) throws Exception { EmbeddedServers.add(EmbeddedServers.defaultIdentifier(), new EmbeddedJettyFactory().withHttpOnly(true)); Spark.staticFiles.location("/public"); - secure("localhost.p12", "changeit", null, null); +// secure("localhost.p12", "changeit", null, null); port(args.length > 0 ? Integer.parseInt(args[0]) : SPARK_DEFAULT_PORT); + var jdbcUrl = args.length > 1 ? args[1] : "jdbc:h2:mem:natter"; + var datasource = JdbcConnectionPool.create( - "jdbc:h2:mem:natter", "natter", "password"); + jdbcUrl, "natter", "password"); createTables(datasource.getConnection()); datasource = JdbcConnectionPool.create( - "jdbc:h2:mem:natter", "natter_api_user", "password"); + jdbcUrl, "natter_api_user", "password"); var database = Database.forDataSource(datasource); var keyPassword = System.getProperty("keystore.password", From 36e4224056758f8fdb22a99710e1c81ea6d50946 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 5 Nov 2019 22:45:20 +0000 Subject: [PATCH 152/209] Use older version of H2 to avoid bug --- natter-api/docker/h2/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/natter-api/docker/h2/Dockerfile b/natter-api/docker/h2/Dockerfile index 5a6b9d9..aa0196a 100644 --- a/natter-api/docker/h2/Dockerfile +++ b/natter-api/docker/h2/Dockerfile @@ -1,7 +1,7 @@ FROM curlimages/curl:7.66.0 AS build-env -ENV RELEASE h2-2019-10-14.zip -ENV SHA256 a72f319f1b5347a6ee9eba42718e69e2ae41e2f846b3475f9292f1e3beb59b01 +ENV RELEASE h2-2018-03-18.zip +ENV SHA256 a45e7824b4f54f5d9d65fb89f22e1e75ecadb15ea4dcf8c5d432b80af59ea759 WORKDIR /tmp @@ -18,5 +18,5 @@ USER 1000:1000 EXPOSE 9092 ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/urandom", \ - "-cp", "/opt/h2/h2-1.4.200.jar", \ + "-cp", "/opt/h2/h2-1.4.197.jar", \ "org.h2.tools.Server", "-tcp", "-tcpAllowOthers"] \ No newline at end of file From 13480e918c7d1149ab2b247260d17e0b29cc8010 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 5 Nov 2019 22:45:44 +0000 Subject: [PATCH 153/209] Remove stray comma --- natter-api/src/main/resources/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index b6de8e3..02f55db 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -48,7 +48,7 @@ INSERT INTO role_permissions(role_id, perms) CREATE TABLE user_roles( space_id INT NOT NULL REFERENCES spaces(space_id), user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), - role_id VARCHAR(30) NOT NULL REFERENCES role_permissions(role_id), + role_id VARCHAR(30) NOT NULL REFERENCES role_permissions(role_id) ); CREATE INDEX user_roles_idx ON user_roles(space_id, user_id); From 16977666a1ebea403c3b6531650cca2f0e334110 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 8 Nov 2019 14:05:58 +0000 Subject: [PATCH 154/209] Create Kubernetes microservice deployments --- .../kubernetes/natter-api-deployment.yaml | 29 +++++++ natter-api/kubernetes/natter-api-service.yaml | 13 ++++ .../natter-database-deployment.yaml | 29 +++++++ .../kubernetes/natter-database-service.yaml | 11 +++ natter-api/kubernetes/natter-namespace.yaml | 6 ++ .../natter-token-database-deployment.yaml | 29 +++++++ .../natter-token-database-service.yaml | 11 +++ .../natter-token-service-deployment.yaml | 29 +++++++ .../kubernetes/natter-token-service.yaml | 11 +++ natter-api/natter.yaml | 13 ---- .../com/manning/apisecurityinaction/Main.java | 31 +++----- .../apisecurityinaction/TokenService.java | 58 ++++++++++++++ .../token/RemoteTokenStore.java | 76 +++++++++++++++++++ .../apisecurityinaction/token/TokenStore.java | 20 +++++ 14 files changed, 332 insertions(+), 34 deletions(-) create mode 100644 natter-api/kubernetes/natter-api-deployment.yaml create mode 100644 natter-api/kubernetes/natter-api-service.yaml create mode 100644 natter-api/kubernetes/natter-database-deployment.yaml create mode 100644 natter-api/kubernetes/natter-database-service.yaml create mode 100644 natter-api/kubernetes/natter-namespace.yaml create mode 100644 natter-api/kubernetes/natter-token-database-deployment.yaml create mode 100644 natter-api/kubernetes/natter-token-database-service.yaml create mode 100644 natter-api/kubernetes/natter-token-service-deployment.yaml create mode 100644 natter-api/kubernetes/natter-token-service.yaml delete mode 100644 natter-api/natter.yaml create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/RemoteTokenStore.java diff --git a/natter-api/kubernetes/natter-api-deployment.yaml b/natter-api/kubernetes/natter-api-deployment.yaml new file mode 100644 index 0000000..be3ca52 --- /dev/null +++ b/natter-api/kubernetes/natter-api-deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: natter-api-deployment + namespace: natter-api +spec: + selector: + matchLabels: + app: natter-api + replicas: 1 + template: + metadata: + labels: + app: natter-api + spec: + securityContext: + runAsNonRoot: true + containers: + - name: natter-api + image: apisecurityinaction/natter-api:latest + imagePullPolicy: Never + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - all + ports: + - containerPort: 4567 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-api-service.yaml b/natter-api/kubernetes/natter-api-service.yaml new file mode 100644 index 0000000..25b0431 --- /dev/null +++ b/natter-api/kubernetes/natter-api-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: natter-api-service + namespace: natter-api +spec: + type: NodePort + selector: + app: natter-api + ports: + - protocol: TCP + port: 4567 + nodePort: 30567 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-database-deployment.yaml b/natter-api/kubernetes/natter-database-deployment.yaml new file mode 100644 index 0000000..763aebd --- /dev/null +++ b/natter-api/kubernetes/natter-database-deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: natter-database-deployment + namespace: natter-api +spec: + selector: + matchLabels: + app: natter-database + replicas: 1 + template: + metadata: + labels: + app: natter-database + spec: + securityContext: + runAsNonRoot: true + containers: + - name: natter-database + image: apisecurityinaction/h2database:latest + imagePullPolicy: Never + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - all + ports: + - containerPort: 9092 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-database-service.yaml b/natter-api/kubernetes/natter-database-service.yaml new file mode 100644 index 0000000..322c1b0 --- /dev/null +++ b/natter-api/kubernetes/natter-database-service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: natter-database-service + namespace: natter-api +spec: + selector: + app: natter-database + ports: + - protocol: TCP + port: 9092 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-namespace.yaml b/natter-api/kubernetes/natter-namespace.yaml new file mode 100644 index 0000000..4faa403 --- /dev/null +++ b/natter-api/kubernetes/natter-namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: natter-api + labels: + name: natter-api diff --git a/natter-api/kubernetes/natter-token-database-deployment.yaml b/natter-api/kubernetes/natter-token-database-deployment.yaml new file mode 100644 index 0000000..51bc258 --- /dev/null +++ b/natter-api/kubernetes/natter-token-database-deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: natter-token-database-deployment + namespace: natter-api +spec: + selector: + matchLabels: + app: natter-token-database + replicas: 1 + template: + metadata: + labels: + app: natter-token-database + spec: + securityContext: + runAsNonRoot: true + containers: + - name: natter-token-database + image: apisecurityinaction/h2database:latest + imagePullPolicy: Never + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - all + ports: + - containerPort: 9092 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-token-database-service.yaml b/natter-api/kubernetes/natter-token-database-service.yaml new file mode 100644 index 0000000..5dbf1bd --- /dev/null +++ b/natter-api/kubernetes/natter-token-database-service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: natter-token-database-service + namespace: natter-api +spec: + selector: + app: natter-token-database + ports: + - protocol: TCP + port: 9092 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-token-service-deployment.yaml b/natter-api/kubernetes/natter-token-service-deployment.yaml new file mode 100644 index 0000000..678ab63 --- /dev/null +++ b/natter-api/kubernetes/natter-token-service-deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: token-service-deployment + namespace: natter-api +spec: + selector: + matchLabels: + app: token-service + replicas: 1 + template: + metadata: + labels: + app: token-service + spec: + securityContext: + runAsNonRoot: true + containers: + - name: token-service + image: apisecurityinaction/token-service:latest + imagePullPolicy: Never + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - all + ports: + - containerPort: 4567 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-token-service.yaml b/natter-api/kubernetes/natter-token-service.yaml new file mode 100644 index 0000000..fa2b971 --- /dev/null +++ b/natter-api/kubernetes/natter-token-service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: natter-token-service + namespace: natter-api +spec: + selector: + app: token-service + ports: + - protocol: TCP + port: 4567 \ No newline at end of file diff --git a/natter-api/natter.yaml b/natter-api/natter.yaml deleted file mode 100644 index 28700c4..0000000 --- a/natter-api/natter.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: natter-api - labels: - app: natter -spec: - containers: - - name: natter-api - image: apisecurityinaction/natter-api - imagePullPolicy: Never - ports: - - containerPort: 4567 \ No newline at end of file diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index b3e9e31..8644da0 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -1,8 +1,6 @@ package com.manning.apisecurityinaction; -import java.io.FileInputStream; import java.net.URI; -import java.security.KeyStore; import java.sql.Connection; import java.util.Set; @@ -31,8 +29,7 @@ public static void main(String... args) throws Exception { port(args.length > 0 ? Integer.parseInt(args[0]) : SPARK_DEFAULT_PORT); - var jdbcUrl = args.length > 1 ? args[1] : "jdbc:h2:mem:natter"; - + var jdbcUrl = "jdbc:h2:tcp://natter-database-service:9092/mem:natter"; var datasource = JdbcConnectionPool.create( jdbcUrl, "natter", "password"); createTables(datasource.getConnection()); @@ -40,16 +37,10 @@ public static void main(String... args) throws Exception { jdbcUrl, "natter_api_user", "password"); var database = Database.forDataSource(datasource); - var keyPassword = System.getProperty("keystore.password", - "changeit").toCharArray(); - var keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(new FileInputStream("keystore.p12"), - keyPassword); - var macKey = keyStore.getKey("hmac-key", keyPassword); - var encKey = keyStore.getKey("aes-key", keyPassword); - - var capController = new CapabilityController( - new MacaroonTokenStore(new JsonTokenStore(), macKey)); + SecureTokenStore tokenStore = new RemoteTokenStore( + "http://natter-token-service:4567/tokens"); + var capController = new CapabilityController(tokenStore); + var tokenController = new TokenController(tokenStore); var spaceController = new SpaceController(database, capController); var userController = new UserController(database); @@ -79,8 +70,6 @@ public static void main(String... args) throws Exception { var clientSecret = "60ho9IS3d6/A+Zzvdn9Y4laiGnI/1TddTM95lEHjArw="; var introspectionEndpoint = URI.create("https://as.example.com:8443/oauth2/introspect"); - SecureTokenStore tokenStore = new CookieTokenStore(); - var tokenController = new TokenController(tokenStore); before(userController::authenticate); before(tokenController::validateToken); @@ -166,13 +155,13 @@ public static void main(String... args) throws Exception { (e, request, response) -> response.status(404)); } - private static void badRequest(Exception ex, + private static void badRequest(Exception ex, Request request, Response response) { - response.status(400); - response.body(new JSONObject().put("error", ex.getMessage()).toString()); - } + response.status(400); + response.body(new JSONObject().put("error", ex.getMessage()).toString()); + } - private static void createTables(Connection connection) throws Exception { + static void createTables(Connection connection) throws Exception { try (var conn = connection; var stmt = conn.createStatement(); var in = Main.class.getResourceAsStream("/schema.sql")) { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java b/natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java new file mode 100644 index 0000000..ca5b8f7 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java @@ -0,0 +1,58 @@ +package com.manning.apisecurityinaction; + +import com.manning.apisecurityinaction.token.DatabaseTokenStore; +import com.manning.apisecurityinaction.token.TokenStore.Token; +import org.dalesbred.Database; +import org.h2.jdbcx.JdbcConnectionPool; +import org.json.JSONObject; +import org.slf4j.*; + +import static spark.Spark.*; + +public class TokenService { + private static final Logger logger = + LoggerFactory.getLogger(TokenService.class); + + public static void main(String... args) throws Exception { + + var jdbcUrl = + "jdbc:h2:tcp://natter-token-database-service:9092/mem:tokens"; + var datasource = JdbcConnectionPool.create( + jdbcUrl, "natter", "password"); + Main.createTables(datasource.getConnection()); + datasource = JdbcConnectionPool.create( + jdbcUrl, "natter_api_user", "password"); + var database = Database.forDataSource(datasource); + var tokenStore = new DatabaseTokenStore(database); + + afterAfter((request, response) -> { + response.header("Content-Type", "application/json"); + }); + + post("/tokens", (request, response) -> { + var json = new JSONObject(request.body()); + var token = Token.fromJson(json); + var tokenId = tokenStore.create(request, token); + logger.info("Created token for user: {}", token.username); + return new JSONObject().put("tokenId", tokenId); + }); + + get("/tokens/:tokenId", (request, response) -> { + logger.info("Validating token"); + var tokenId = request.params(":tokenId"); + return tokenStore.read(request, tokenId) + .map(Token::toJson) + .orElseGet(() -> { + response.status(404); + return new JSONObject(); + }); + }); + + delete("/tokens/:tokenId", (request, response) -> { + var tokenId = request.params(":tokenId"); + logger.info("Revoking token: {}", tokenId); + tokenStore.revoke(request, tokenId); + return new JSONObject(); + }); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/RemoteTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/RemoteTokenStore.java new file mode 100644 index 0000000..69d2822 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/RemoteTokenStore.java @@ -0,0 +1,76 @@ +package com.manning.apisecurityinaction.token; + +import java.io.IOException; +import java.net.URI; +import java.net.http.*; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.Optional; + +import org.json.JSONObject; +import spark.Request; + +public class RemoteTokenStore implements SecureTokenStore { + + private final URI tokenServiceUri; + private final HttpClient httpClient; + + public RemoteTokenStore(String tokenServiceUri) { + this.tokenServiceUri = URI.create(tokenServiceUri); + this.httpClient = HttpClient.newBuilder().build(); + } + + @Override + public String create(Request request, Token token) { + var json = token.toJson(); + var httpRequest = HttpRequest.newBuilder(tokenServiceUri) + .POST(BodyPublishers.ofString(json.toString())) + .build(); + + return send(httpRequest).getString("tokenId"); + } + + @Override + public Optional read(Request request, String tokenId) { + var httpRequest = HttpRequest.newBuilder() + .uri(tokenServiceUri.resolve("/tokens/" + tokenId)) + .GET() + .build(); + + try { + var tokenJson = send(httpRequest); + var token = Token.fromJson(tokenJson); + return Optional.of(token); + } catch (RuntimeException e) { + return Optional.empty(); + } + } + + @Override + public void revoke(Request request, String tokenId) { + var httpRequest = HttpRequest.newBuilder() + .uri(tokenServiceUri.resolve("/tokens/" + tokenId)) + .DELETE() + .build(); + + send(httpRequest); + } + + private JSONObject send(HttpRequest request) { + try { + var response = httpClient.send(request, + BodyHandlers.ofString()); + if (response.statusCode() / 100 != 2) { + throw new RuntimeException( + "Bad response from token service: " + + response.statusCode()); + } + return new JSONObject(response.body()); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java index 647dd5d..2110af1 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java @@ -1,5 +1,6 @@ package com.manning.apisecurityinaction.token; +import org.json.JSONObject; import spark.Request; import java.time.Instant; @@ -22,6 +23,25 @@ public Token(Instant expiry, String username) { this.username = username; this.attributes = new ConcurrentHashMap<>(); } + + public JSONObject toJson() { + return new JSONObject() + .put("exp", expiry.getEpochSecond()) + .put("sub", username) + .put("attrs", attributes); + } + + public static Token fromJson(JSONObject json) { + var expiry = Instant.ofEpochSecond(json.getLong("exp")); + var user = json.optString("sub"); + var attrs = new LinkedHashMap(); + json.getJSONObject("attrs").toMap() + .forEach((key, value) -> attrs.put(key, value.toString())); + + var token = new Token(expiry, user); + token.attributes.putAll(attrs); + return token; + } } } From 60d1bbb3572b666ecec792eeaad3afa8b21b7a32 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 9 Nov 2019 13:43:54 +0000 Subject: [PATCH 155/209] Add network policies --- .../network/network-policy-default-deny.yaml | 10 ++++++ .../network/network-policy-natter-api.yaml | 31 +++++++++++++++++++ .../network-policy-natter-database.yaml | 20 ++++++++++++ .../network-policy-token-database.yaml | 20 ++++++++++++ .../network/network-policy-token-service.yaml | 28 +++++++++++++++++ 5 files changed, 109 insertions(+) create mode 100644 natter-api/kubernetes/network/network-policy-default-deny.yaml create mode 100644 natter-api/kubernetes/network/network-policy-natter-api.yaml create mode 100644 natter-api/kubernetes/network/network-policy-natter-database.yaml create mode 100644 natter-api/kubernetes/network/network-policy-token-database.yaml create mode 100644 natter-api/kubernetes/network/network-policy-token-service.yaml diff --git a/natter-api/kubernetes/network/network-policy-default-deny.yaml b/natter-api/kubernetes/network/network-policy-default-deny.yaml new file mode 100644 index 0000000..db922d4 --- /dev/null +++ b/natter-api/kubernetes/network/network-policy-default-deny.yaml @@ -0,0 +1,10 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-network-policy + namespace: natter-api +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress \ No newline at end of file diff --git a/natter-api/kubernetes/network/network-policy-natter-api.yaml b/natter-api/kubernetes/network/network-policy-natter-api.yaml new file mode 100644 index 0000000..c2d7c87 --- /dev/null +++ b/natter-api/kubernetes/network/network-policy-natter-api.yaml @@ -0,0 +1,31 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: token-api-network-policy + namespace: natter-api +spec: + podSelector: + matchLabels: + app: natter-api + policyTypes: + - Ingress + - Egress + ingress: + - ports: + - protocol: TCP + port: 4567 + egress: + - to: + - podSelector: + matchLabels: + app: natter-database + ports: + - protocol: TCP + port: 9092 + - to: + - podSelector: + matchLabels: + app: token-service + ports: + - protocol: TCP + port: 4567 diff --git a/natter-api/kubernetes/network/network-policy-natter-database.yaml b/natter-api/kubernetes/network/network-policy-natter-database.yaml new file mode 100644 index 0000000..b557b19 --- /dev/null +++ b/natter-api/kubernetes/network/network-policy-natter-database.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: natter-database-network-policy + namespace: natter-api +spec: + podSelector: + matchLabels: + app: natter-database + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + app: natter-api + ports: + - protocol: TCP + port: 9092 diff --git a/natter-api/kubernetes/network/network-policy-token-database.yaml b/natter-api/kubernetes/network/network-policy-token-database.yaml new file mode 100644 index 0000000..639e252 --- /dev/null +++ b/natter-api/kubernetes/network/network-policy-token-database.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: token-database-network-policy + namespace: natter-api +spec: + podSelector: + matchLabels: + app: natter-token-database + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + app: token-service + ports: + - protocol: TCP + port: 9092 diff --git a/natter-api/kubernetes/network/network-policy-token-service.yaml b/natter-api/kubernetes/network/network-policy-token-service.yaml new file mode 100644 index 0000000..3b6538f --- /dev/null +++ b/natter-api/kubernetes/network/network-policy-token-service.yaml @@ -0,0 +1,28 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: token-service-network-policy + namespace: natter-api +spec: + podSelector: + matchLabels: + app: token-service + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: + matchLabels: + app: natter-api + ports: + - protocol: TCP + port: 4567 + egress: + - to: + - podSelector: + matchLabels: + app: natter-token-database + ports: + - protocol: TCP + port: 9092 From b06254c496801fa132bd7994f4b9ee5eef6f6d0f Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 11 Nov 2019 22:57:31 +0000 Subject: [PATCH 156/209] Expose certs as secrets --- natter-api/kubernetes/natter-api-deployment.yaml | 15 +++++++++++++++ .../kubernetes/natter-database-deployment.yaml | 15 +++++++++++++++ .../natter-token-database-deployment.yaml | 15 +++++++++++++++ .../natter-token-service-deployment.yaml | 15 +++++++++++++++ natter-api/natter-api-service.p12 | Bin 0 -> 4015 bytes natter-api/natter-database-service.p12 | Bin 0 -> 4023 bytes natter-api/natter-token-database-service.p12 | Bin 0 -> 4031 bytes natter-api/natter-token-service.p12 | Bin 0 -> 4015 bytes natter-api/root-ca.p12 | Bin 0 -> 1506 bytes 9 files changed, 60 insertions(+) create mode 100644 natter-api/natter-api-service.p12 create mode 100644 natter-api/natter-database-service.p12 create mode 100644 natter-api/natter-token-database-service.p12 create mode 100644 natter-api/natter-token-service.p12 create mode 100644 natter-api/root-ca.p12 diff --git a/natter-api/kubernetes/natter-api-deployment.yaml b/natter-api/kubernetes/natter-api-deployment.yaml index be3ca52..920ae43 100644 --- a/natter-api/kubernetes/natter-api-deployment.yaml +++ b/natter-api/kubernetes/natter-api-deployment.yaml @@ -15,10 +15,25 @@ spec: spec: securityContext: runAsNonRoot: true + volumes: + - name: root-ca-cert + secret: + secretName: root-ca-cert + - name: natter-api-cert + secret: + secretName: natter-api-service-cert + defaultMode: 256 containers: - name: natter-api image: apisecurityinaction/natter-api:latest imagePullPolicy: Never + volumeMounts: + - name: root-ca-cert + mountPath: "/etc/certs/root-ca" + readOnly: true + - name: natter-api-cert + mountPath: "/etc/certs/natter-api" + readOnly: true securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true diff --git a/natter-api/kubernetes/natter-database-deployment.yaml b/natter-api/kubernetes/natter-database-deployment.yaml index 763aebd..5d9018a 100644 --- a/natter-api/kubernetes/natter-database-deployment.yaml +++ b/natter-api/kubernetes/natter-database-deployment.yaml @@ -15,10 +15,25 @@ spec: spec: securityContext: runAsNonRoot: true + volumes: + - name: root-ca-cert + secret: + secretName: root-ca-cert + - name: natter-database-cert + secret: + secretName: natter-database-service-cert + defaultMode: 256 containers: - name: natter-database image: apisecurityinaction/h2database:latest imagePullPolicy: Never + volumeMounts: + - name: root-ca-cert + mountPath: "/etc/certs/root-ca" + readOnly: true + - name: natter-database-cert + mountPath: "/etc/certs/natter-database" + readOnly: true securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true diff --git a/natter-api/kubernetes/natter-token-database-deployment.yaml b/natter-api/kubernetes/natter-token-database-deployment.yaml index 51bc258..fcf7686 100644 --- a/natter-api/kubernetes/natter-token-database-deployment.yaml +++ b/natter-api/kubernetes/natter-token-database-deployment.yaml @@ -15,10 +15,25 @@ spec: spec: securityContext: runAsNonRoot: true + volumes: + - name: root-ca-cert + secret: + secretName: root-ca-cert + - name: natter-token-database-cert + secret: + secretName: natter-token-database-service-cert + defaultMode: 256 containers: - name: natter-token-database image: apisecurityinaction/h2database:latest imagePullPolicy: Never + volumeMounts: + - name: root-ca-cert + mountPath: "/etc/certs/root-ca" + readOnly: true + - name: natter-token-database-cert + mountPath: "/etc/certs/natter-token-database" + readOnly: true securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true diff --git a/natter-api/kubernetes/natter-token-service-deployment.yaml b/natter-api/kubernetes/natter-token-service-deployment.yaml index 678ab63..a80c9f8 100644 --- a/natter-api/kubernetes/natter-token-service-deployment.yaml +++ b/natter-api/kubernetes/natter-token-service-deployment.yaml @@ -15,10 +15,25 @@ spec: spec: securityContext: runAsNonRoot: true + volumes: + - name: root-ca-cert + secret: + secretName: root-ca-cert + - name: natter-token-cert + secret: + secretName: natter-token-service-cert + defaultMode: 256 containers: - name: token-service image: apisecurityinaction/token-service:latest imagePullPolicy: Never + volumeMounts: + - name: root-ca-cert + mountPath: "/etc/certs/root-ca" + readOnly: true + - name: natter-token-cert + mountPath: "/etc/certs/natter-token-service" + readOnly: true securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true diff --git a/natter-api/natter-api-service.p12 b/natter-api/natter-api-service.p12 new file mode 100644 index 0000000000000000000000000000000000000000..9f78b3b87386a1bf54b60091bd649bb59c6059f6 GIT binary patch literal 4015 zcmV;g4^Z$hf)A?#0Ru3C4|fI$Duzgg_YDCD0ic2pXas@}WH5pcU@(FT7X}F`hDe6@ z4FLxRpn?hrFoFsM0s#Opf(iWw2`Yw2hW8Bt2LUh~1_~;MNQUeefN4cMw7a9dvAR$zYQBOkT(iW6jnH@$>AE+f{M;i#m!1Cep3 zup(;-G_6w&;?@h22{O|peV%9p0VQ|I{kL7jvfS_Jy@=ELV-)OIAY-I0EKMVLCBvaq zB&5M8*j-#EcCBEu%;nCOcTc(MdUEsILpIFvH1y0&()bXsjf5U5Mf@dBYDh)lbTJ!dpI|~Fl z7!QH3O^)4@`oXZQqIPsyI=@LTE6e1c*Ak5s9&oo|j=`}Pd0ud+%V2HwyqlA(8j%>l zw>W*`*7ne*V}(Psx+JB*S#=E}<$|y$)3@FOReqGaf{_?&itJ#(F5Tb?51{OWu>+ShCrX?vlGonx|LEPuPQJH4R1H;$( z29BF~*u~$ZF6hLv_s}@GyG|j~wcCkK>>R;T8ttvWP{=ZDAj!{hKZR`HN_7+}A&0&n zgyXcS&nE_&hUf*HU87cwCeH0CsSZhN(@_su%VP_|{X`B$(5>QZidhJOqr~obgupQ~ zFWlkfW*@3z>)Nzf$HcqN-41p8#7d^##dY|65$$pH;)MGiA z7hbbSr&n~4;2RJF&s*0zq}Xv4!TqlMjv|&QdfoUr(56s*KER=Cx? zauVD}(Qcb@i35Y++hD1PQA~}|=7$;k)62swch6Pp)oCWMmA^Mnh;}VdNIFxB)|+eo z(>@4nzx1b(3Q2DlPgKLX&iG(y!5akPh+-Tvc0B<^GFVnNJ8L!>mJjc{UBY&`B4(N5 zSgaYk7y9Arp)sADhDIkC&_!0))BnMpPVxdV7&q~XBr!SXh+u*N9zY*uE439HI6V({ zga^;D0fKJPA(9YK{Z_OTXa{RFnb35@BV=wH51%jm`=E2lI`F!Px@hNu*Gs;LNpRTB z@o{!Z^yMBJxGqb57~#QkC9(n3#-7l}Fhr>s6y!>Ol9|bf-M}u%(`0C{!!!-!UPuk^A;-;LomAN8WBF*+0iAR&B8Mo(Z+B}!qC%{>uPqnFoOt3b_?g7 zag)MXr_FrK`ktd%@=MK)_QNceDYV|XI6)ygrod*{`*gl1 z0Rb=?mgMavLEhR27wjH$L76?UowgBR7{7npzT*RMvGG$wYP>{Q943?*G{M%znTfnm znS=iQE|@qWP@R^?0@YWl#PE+nFRPRDqP3VN3Y^sY_Qms`cD#4rGQiW=7~U;u0P)<1 z)#6l8M=foCgxY)QS@#n#al!f!=rrPlJ`XABC%B{+43*JO4DYw%YW<;!?diIkaLBBr z_oX=6tE-Gm=bOuZck};LyyEyi&{pn7tT)mnBq^)dJEs`V%zSmZ=1I04LA#1JyL_%` zeI$lbada;aZrM<|Hb!p>OQ5~szX}Ck z=(086kbpKxyXNA9?2!>o^e4OKW~VyvN2uR^U+T9H%2ecQ@u>}lV(+4$DJbept68x( z9D85va7UlKX3rRXs5%CC*&X!EKT(P!_MMX{$t9!?mO)BsV`@OxC#^1;k=T5Oh;owY zJa1OV5HC=mIOZM$DIVQ+u*V$wVVnTwojbo>y?Pw!?=01JKx4_p0VJP7K9~)>Vf)Fo z^C5~44V(fwPzS+>em^`zhV1pj?qp>wjMTHwFd2R6@2J(3(UH;GfHMF9Ctw$qr!6f(a9&_Pr@J>WE54qAtBrJVMI zRP#@DD+2mjfvmsm32I?a1}X{zC?-hX(Innbkpg4+gr!5+W5h8g4?H|1y|*%5IxsH88I@f^Li$5vB3ZwzN)l}H42mMoVdX#&Zqi5fn+zpNo5C^FS>Uux z(z(~pHBOJ}e`5Gf+vIDP1f~X6&a**1$ppI=nNKz;FH9;MUhSVTCgdszvq?HXK?RWg zAb7PnU$p6T_&wBfaJ8%`YVpSUcN+F%`RPmv7Ykpav(A?Jvagm6QukWVSpb~3&~G$K z;2QakEaQp~j{yM$ZR$C>=*Gf7kB*BO$R9z0TzO4*Ou&DWEnTiI(aS4&-Lb~5U)aWb zA*BsQ(-aCv+z}8K_3c7u316{I;qeA655ac5F;~L6TZ2C$FQf?avrGbBS@aP7<>}r1*?qU*!tLHQ6p80cTN?i5uV=YIvFJ^vGYIv z>fQ&nNPGi>U!CJEhf%LT!$6^w-^tMHnNfPqWX(EuEJ7f=O%d01xnZj{NeiMDZ1@CM z@=Jy<%{q}yh_rJQqVq2KA=Y9m?eG!gIV6 zs%%w7)Q~F-Nz_p#ntZ%z;k^DQy&g_{QNpgdoQSyBm0BIFYL8uKkv0b^a+41ih7dfm zN$e(s0@O0mE6yYBGP7DNcD{?_Y-NV;VFA zM|oX&?IOTfWR17M6xpulb`+DAR(uR{?CR7vFoFd^1_>&LNQUfvPjwN?SK|9F$6)eG2SL4==?R^7f8)xm;%%pKV5>X+{6~0AVIObqn zrv*3%+v5hiCy5)a#Ib&)Yh4UoSL0^%q&l4Z2e*!RE18Cm^$E|U2|fcgY-&>2I2oB> z9$gBI*())+L!-h98R??deR(k*o16BFw^*99^`L?`;mCT(oW3j${w}lPz(^hz6HO44 z4LYjqCmkxkYX(UNug30(>VqeR@gji4i5EyUE~(xD3c&)%$4rt=DTAP7lTSDO(~8^x**wrrzc`AD6l92D>#t7BRJCGg?!_n3 zd8CU(Z_~EzQnDXkEvXz*n;V|5uYBlg^N<^1 zm1A%m)S!j+EmAN(9U`Qa%?cC^7+}P699A#ZdY>8QvIanpXh`@X$rN#IVTpT~U>1Z4 z?I(+Hcz1id`@GtO(QxLo`6P-*xmIfH{Jj-`7S>fL2Qu_(x(JDgSSeVt4YD-(P>zf? zIKJVRm>4|2V$)fIs_}Mhg`=?VLu25j`M?id^UaXxQz)j|mHkNVrjMb-%;FrjH!ep; z5vGH)SK{7@`Wyc0=x*t8WrqK2EnkV;hxz|L$TG7{@3O1wfY;JAm~TU@jL)*R=1+>8 zX_P-S8jzfTf{j!;?NCUeaW=@n@;PXK{`eM^>)?`UESPpa34J)%s%&LL?tH;ZFBrrf zi$G3Vk!iHg)^;m#A28i^R|D1B-ftWJsuS`83BJ=f-68a}2E$$~r2fg~uEp(2eXhDc z`(XCRH>Kl?s0{NG&~{OL<@j7{mmLI?Pj2qthV)k~=N`^8|S{#KO#_+r-TVE)yPGrM&*UlUxL!`jL+j zdyuqTW5NGCN@OAbllFUP9i9>CyAtiO@kDtP8!@ z9+<7Ltc_A;u}=um3dc&(E`=91tcj<08O<^7Y>7$6r%YbSm+T{N$`j1q?GLm`_60cl z12kvq`-fgpyC(<@cg(4`>o9a~G&2FxZ)1kLi@MM|S!}ghe4;TWFe3&DDuzgg_YDCF z6)_eB6i(+Mccx>V<;_>r9oXXol7PAmFEA@GA20_71uG5%0vZGq+kEbgOSx8PH}CFi VEFsDk`fNpc1PHLQ$H%P(otidyq)z|< literal 0 HcmV?d00001 diff --git a/natter-api/natter-database-service.p12 b/natter-api/natter-database-service.p12 new file mode 100644 index 0000000000000000000000000000000000000000..f8d8dc7e0c00ae7a9b9fc8dd2f4db7429c223d10 GIT binary patch literal 4023 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 literal 0 HcmV?d00001 diff --git a/natter-api/natter-token-service.p12 b/natter-api/natter-token-service.p12 new file mode 100644 index 0000000000000000000000000000000000000000..429a4496d233504c9b67ff9c3171ac1c984ef590 GIT binary patch literal 4015 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 literal 0 HcmV?d00001 diff --git a/natter-api/root-ca.p12 b/natter-api/root-ca.p12 new file mode 100644 index 0000000000000000000000000000000000000000..3624f145ad39af792cd69bc28f9db41f7a991ff1 GIT binary patch literal 1506 zcmV<81s(b@f(70J0Ru3C1(ya1Duzgg_YDCD0ic2fhy;QKgfM~yfG~mud=mju=P9@RHwKdUCHgI-Qsg zoxV2x!FrZT^6rQm%rcz%BQ@7jB_8uQMZ55ofp#N8If!440p{j4RnTVeAUYusls7yF z?Cb4T3#i$JYETAz3SD)1GB(G~jZ-!CI@^B@(JbABW%5tj|A!MNKUKQ_ASyB*Gq0fW zY97ROOiX!COm(^#;*$|9KBk>#4X$B1fgvwo!hf-t}!x_f-1 z2DVj6(ey+W9C-n_7SnnyFgS4rLBPUINB8SJ=usYtNM2So%;?q>Hv)o|t>Nyggq7>MB4kpO3~`1+ADMynhyB+K{@w8+g8&%JIME#oP~Hl~gv z{1f-hX+hZ`z?TPvDqOLe_AL`Nx_#TOu$XM0%3yK^;`RTd=<8*+`G(qG*_%^NtrDpo?Y=t{;wn<$A{CIWL4r40FtGC3Nx^c8! z3-YN@u3Xl_uMwWS8i{SqYuO}z%0GHZiAC7nL3JN)6c>eOa$h}}^lz$n5^x(#{KdZc zUGW75>z&*t!vX?-)CuV%{>n5izkUXfq_;+Z^rM5&`9#&DP;-Y(i;!1WefH0!?sd1wjBYQ^}PLZ zo;Dft#bZ@5;|q3Msw~8G!_ad?WrzNoJh>s^6<4ew>;lcgXAR}S#nycYg7Azf$U;Wk z7Fh`b?ULZkL00~=N8uUHHfqn@)|eTUhw#_+{60-Ptf7iQp250rWsq zSXp4FQ6(VHtVLdY^moi6_?sVpt2<(kdA)W+!naER$V|01d%tMylnhyPcsK*!IFA{v9NR!9Zn zIFMxc&<&9RhN2P1m94GpFAavMMdg;7qIjobO^b-~EjZysu>)kOfRxoVdE*)2#)Gc} zzxRsJ`t*6_26s=is?ssWlm}f$F~!+WA-|mGo**G}>RkA1kOn z?$Lw)9U3U1!NYt7okgLQJ^+P^3I>u2$Lr1(8M;71?R@!bDVt*Y`8+kP(oBcE3LSpr zs*e-odw`~(GIDo{@3ev41}1{d%)TFVz|G`L6T+e`k(8RAt#KxC_~LR|P&Nb?p6|U7 z=3lw?W^(s@=g1)CX78VwmTYA8cbt)??LxDJGE-z@ zBt_zCAGz^VtVHAjleP<{rHPK%9zl!Ivc~AV$M-h`T?C$APGi3k-}It+PHVnMF|h9=1`ln z$ZroFw}OALE_wKFcJRaS>R2afd^f=F`!AEG%XBmYb;J4*v)96V#c+_Mi0$HvTKy_t z!44Edi|Jz9kll6J@c!x{TxZ_4!3v4WM0;7jLgW|FR*R#29D;DcD@*JYZsg4bt97~X zYTtW&1D?nGjESY6c2-u~6}S1L98szK;v==r`OV literal 0 HcmV?d00001 From 97ca77b3a337d32c1b1fa3c4be49d5d3d121bdf8 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 12 Nov 2019 15:10:52 +0000 Subject: [PATCH 157/209] Enable TLS across the cluster --- natter-api/docker/h2/Dockerfile | 7 ++++++- natter-api/kubernetes/natter-api-deployment.yaml | 1 + natter-api/kubernetes/natter-database-deployment.yaml | 5 +++-- .../kubernetes/natter-token-database-deployment.yaml | 5 +++-- natter-api/kubernetes/natter-token-service-deployment.yaml | 3 ++- natter-api/pom.xml | 6 ++++++ .../main/java/com/manning/apisecurityinaction/Main.java | 7 ++++--- .../java/com/manning/apisecurityinaction/TokenService.java | 5 +++-- 8 files changed, 28 insertions(+), 11 deletions(-) diff --git a/natter-api/docker/h2/Dockerfile b/natter-api/docker/h2/Dockerfile index aa0196a..a6b8184 100644 --- a/natter-api/docker/h2/Dockerfile +++ b/natter-api/docker/h2/Dockerfile @@ -14,9 +14,14 @@ FROM gcr.io/distroless/java:11 WORKDIR /opt COPY --from=build-env /tmp/h2/bin /opt/h2 +ARG KEYSTORE +ENV JAVA_TOOL_OPTIONS -Djavax.net.ssl.keyStore=${KEYSTORE} \ + -Djavax.net.ssl.keyStorePassword=changeit \ + -Dh2.enableAnonymousTLS=false + USER 1000:1000 EXPOSE 9092 ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/urandom", \ "-cp", "/opt/h2/h2-1.4.197.jar", \ - "org.h2.tools.Server", "-tcp", "-tcpAllowOthers"] \ No newline at end of file + "org.h2.tools.Server", "-tcp", "-tcpAllowOthers", "-tcpSSL"] \ No newline at end of file diff --git a/natter-api/kubernetes/natter-api-deployment.yaml b/natter-api/kubernetes/natter-api-deployment.yaml index 920ae43..d41b4e9 100644 --- a/natter-api/kubernetes/natter-api-deployment.yaml +++ b/natter-api/kubernetes/natter-api-deployment.yaml @@ -15,6 +15,7 @@ spec: spec: securityContext: runAsNonRoot: true + fsGroup: 1000 volumes: - name: root-ca-cert secret: diff --git a/natter-api/kubernetes/natter-database-deployment.yaml b/natter-api/kubernetes/natter-database-deployment.yaml index 5d9018a..780a382 100644 --- a/natter-api/kubernetes/natter-database-deployment.yaml +++ b/natter-api/kubernetes/natter-database-deployment.yaml @@ -15,6 +15,7 @@ spec: spec: securityContext: runAsNonRoot: true + fsGroup: 1000 volumes: - name: root-ca-cert secret: @@ -22,10 +23,10 @@ spec: - name: natter-database-cert secret: secretName: natter-database-service-cert - defaultMode: 256 + defaultMode: 288 containers: - name: natter-database - image: apisecurityinaction/h2database:latest + image: apisecurityinaction/h2database:tls imagePullPolicy: Never volumeMounts: - name: root-ca-cert diff --git a/natter-api/kubernetes/natter-token-database-deployment.yaml b/natter-api/kubernetes/natter-token-database-deployment.yaml index fcf7686..5914378 100644 --- a/natter-api/kubernetes/natter-token-database-deployment.yaml +++ b/natter-api/kubernetes/natter-token-database-deployment.yaml @@ -15,6 +15,7 @@ spec: spec: securityContext: runAsNonRoot: true + fsGroup: 1000 volumes: - name: root-ca-cert secret: @@ -22,10 +23,10 @@ spec: - name: natter-token-database-cert secret: secretName: natter-token-database-service-cert - defaultMode: 256 + defaultMode: 288 containers: - name: natter-token-database - image: apisecurityinaction/h2database:latest + image: apisecurityinaction/tokendatabase:tls imagePullPolicy: Never volumeMounts: - name: root-ca-cert diff --git a/natter-api/kubernetes/natter-token-service-deployment.yaml b/natter-api/kubernetes/natter-token-service-deployment.yaml index a80c9f8..97f475e 100644 --- a/natter-api/kubernetes/natter-token-service-deployment.yaml +++ b/natter-api/kubernetes/natter-token-service-deployment.yaml @@ -15,6 +15,7 @@ spec: spec: securityContext: runAsNonRoot: true + fsGroup: 1000 volumes: - name: root-ca-cert secret: @@ -22,7 +23,7 @@ spec: - name: natter-token-cert secret: secretName: natter-token-service-cert - defaultMode: 256 + defaultMode: 288 containers: - name: token-service image: apisecurityinaction/token-service:latest diff --git a/natter-api/pom.xml b/natter-api/pom.xml index 9a50df9..5e08de6 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -12,6 +12,7 @@ com.manning.apisecurityinaction.Main 7.26.0.Final + /etc/certs/natter-api/natter-api-service.p12 @@ -95,6 +96,11 @@ ${exec.mainClass} -Djava.security.egd=file:/dev/urandom + -Djavax.net.ssl.trustStore=/etc/certs/root-ca/root-ca.p12 + -Djavax.net.ssl.trustStorePassword=changeit + -Djavax.net.ssl.keyStore=${keystore.path} + -Djavax.net.ssl.keyStorePassword=changeit + -Dh2.enableAnonymousTLS=false 4567 diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 8644da0..e27e99c 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -25,11 +25,12 @@ public static void main(String... args) throws Exception { EmbeddedServers.add(EmbeddedServers.defaultIdentifier(), new EmbeddedJettyFactory().withHttpOnly(true)); Spark.staticFiles.location("/public"); -// secure("localhost.p12", "changeit", null, null); + secure("/etc/certs/natter-api/natter-api-service.p12", + "changeit", null, null); port(args.length > 0 ? Integer.parseInt(args[0]) : SPARK_DEFAULT_PORT); - var jdbcUrl = "jdbc:h2:tcp://natter-database-service:9092/mem:natter"; + var jdbcUrl = "jdbc:h2:ssl://natter-database-service:9092/mem:natter"; var datasource = JdbcConnectionPool.create( jdbcUrl, "natter", "password"); createTables(datasource.getConnection()); @@ -38,7 +39,7 @@ public static void main(String... args) throws Exception { var database = Database.forDataSource(datasource); SecureTokenStore tokenStore = new RemoteTokenStore( - "http://natter-token-service:4567/tokens"); + "https://natter-token-service:4567/tokens"); var capController = new CapabilityController(tokenStore); var tokenController = new TokenController(tokenStore); var spaceController = new SpaceController(database, capController); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java b/natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java index ca5b8f7..71fe0b5 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java @@ -14,9 +14,10 @@ public class TokenService { LoggerFactory.getLogger(TokenService.class); public static void main(String... args) throws Exception { - + secure("/etc/certs/natter-token-service/natter-token-service.p12", + "changeit", null, null); var jdbcUrl = - "jdbc:h2:tcp://natter-token-database-service:9092/mem:tokens"; + "jdbc:h2:ssl://natter-token-database-service:9092/mem:tokens"; var datasource = JdbcConnectionPool.create( jdbcUrl, "natter", "password"); Main.createTables(datasource.getConnection()); From 2ba658ba44337192c3291308eb6bc63a8ee7ab4a Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 17 Nov 2019 17:24:45 +0000 Subject: [PATCH 158/209] Disable TLS again and switch to link preview microservice example --- natter-api/docker/h2/Dockerfile | 7 +-- .../kubernetes/natter-api-deployment.yaml | 16 ------ .../natter-database-deployment.yaml | 18 +------ ...atter-link-preview-service-deployment.yaml | 29 ++++++++++ ....yaml => natter-link-preview-service.yaml} | 4 +- .../natter-token-database-deployment.yaml | 45 ---------------- .../natter-token-database-service.yaml | 11 ---- .../natter-token-service-deployment.yaml | 45 ---------------- ....yaml => network-policy-link-preview.yaml} | 10 ++-- .../network/network-policy-token-service.yaml | 28 ---------- natter-api/pom.xml | 11 ++-- .../LinkPreviewService.java | 53 +++++++++++++++++++ .../com/manning/apisecurityinaction/Main.java | 29 +++++----- .../controller/SpaceController.java | 41 +++++++++++++- 14 files changed, 150 insertions(+), 197 deletions(-) create mode 100644 natter-api/kubernetes/natter-link-preview-service-deployment.yaml rename natter-api/kubernetes/{natter-token-service.yaml => natter-link-preview-service.yaml} (65%) delete mode 100644 natter-api/kubernetes/natter-token-database-deployment.yaml delete mode 100644 natter-api/kubernetes/natter-token-database-service.yaml delete mode 100644 natter-api/kubernetes/natter-token-service-deployment.yaml rename natter-api/kubernetes/network/{network-policy-token-database.yaml => network-policy-link-preview.yaml} (68%) delete mode 100644 natter-api/kubernetes/network/network-policy-token-service.yaml create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java diff --git a/natter-api/docker/h2/Dockerfile b/natter-api/docker/h2/Dockerfile index a6b8184..aa0196a 100644 --- a/natter-api/docker/h2/Dockerfile +++ b/natter-api/docker/h2/Dockerfile @@ -14,14 +14,9 @@ FROM gcr.io/distroless/java:11 WORKDIR /opt COPY --from=build-env /tmp/h2/bin /opt/h2 -ARG KEYSTORE -ENV JAVA_TOOL_OPTIONS -Djavax.net.ssl.keyStore=${KEYSTORE} \ - -Djavax.net.ssl.keyStorePassword=changeit \ - -Dh2.enableAnonymousTLS=false - USER 1000:1000 EXPOSE 9092 ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/urandom", \ "-cp", "/opt/h2/h2-1.4.197.jar", \ - "org.h2.tools.Server", "-tcp", "-tcpAllowOthers", "-tcpSSL"] \ No newline at end of file + "org.h2.tools.Server", "-tcp", "-tcpAllowOthers"] \ No newline at end of file diff --git a/natter-api/kubernetes/natter-api-deployment.yaml b/natter-api/kubernetes/natter-api-deployment.yaml index d41b4e9..be3ca52 100644 --- a/natter-api/kubernetes/natter-api-deployment.yaml +++ b/natter-api/kubernetes/natter-api-deployment.yaml @@ -15,26 +15,10 @@ spec: spec: securityContext: runAsNonRoot: true - fsGroup: 1000 - volumes: - - name: root-ca-cert - secret: - secretName: root-ca-cert - - name: natter-api-cert - secret: - secretName: natter-api-service-cert - defaultMode: 256 containers: - name: natter-api image: apisecurityinaction/natter-api:latest imagePullPolicy: Never - volumeMounts: - - name: root-ca-cert - mountPath: "/etc/certs/root-ca" - readOnly: true - - name: natter-api-cert - mountPath: "/etc/certs/natter-api" - readOnly: true securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true diff --git a/natter-api/kubernetes/natter-database-deployment.yaml b/natter-api/kubernetes/natter-database-deployment.yaml index 780a382..763aebd 100644 --- a/natter-api/kubernetes/natter-database-deployment.yaml +++ b/natter-api/kubernetes/natter-database-deployment.yaml @@ -15,26 +15,10 @@ spec: spec: securityContext: runAsNonRoot: true - fsGroup: 1000 - volumes: - - name: root-ca-cert - secret: - secretName: root-ca-cert - - name: natter-database-cert - secret: - secretName: natter-database-service-cert - defaultMode: 288 containers: - name: natter-database - image: apisecurityinaction/h2database:tls + image: apisecurityinaction/h2database:latest imagePullPolicy: Never - volumeMounts: - - name: root-ca-cert - mountPath: "/etc/certs/root-ca" - readOnly: true - - name: natter-database-cert - mountPath: "/etc/certs/natter-database" - readOnly: true securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true diff --git a/natter-api/kubernetes/natter-link-preview-service-deployment.yaml b/natter-api/kubernetes/natter-link-preview-service-deployment.yaml new file mode 100644 index 0000000..b1e2816 --- /dev/null +++ b/natter-api/kubernetes/natter-link-preview-service-deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: link-preview-service-deployment + namespace: natter-api +spec: + selector: + matchLabels: + app: link-preview-service + replicas: 1 + template: + metadata: + labels: + app: link-preview-service + spec: + securityContext: + runAsNonRoot: true + containers: + - name: link-preview-service + image: apisecurityinaction/link-preview-service:latest + imagePullPolicy: Never + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - all + ports: + - containerPort: 4567 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-token-service.yaml b/natter-api/kubernetes/natter-link-preview-service.yaml similarity index 65% rename from natter-api/kubernetes/natter-token-service.yaml rename to natter-api/kubernetes/natter-link-preview-service.yaml index fa2b971..bd0f401 100644 --- a/natter-api/kubernetes/natter-token-service.yaml +++ b/natter-api/kubernetes/natter-link-preview-service.yaml @@ -1,11 +1,11 @@ apiVersion: v1 kind: Service metadata: - name: natter-token-service + name: natter-link-preview-service namespace: natter-api spec: selector: - app: token-service + app: link-preview-service ports: - protocol: TCP port: 4567 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-token-database-deployment.yaml b/natter-api/kubernetes/natter-token-database-deployment.yaml deleted file mode 100644 index 5914378..0000000 --- a/natter-api/kubernetes/natter-token-database-deployment.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: natter-token-database-deployment - namespace: natter-api -spec: - selector: - matchLabels: - app: natter-token-database - replicas: 1 - template: - metadata: - labels: - app: natter-token-database - spec: - securityContext: - runAsNonRoot: true - fsGroup: 1000 - volumes: - - name: root-ca-cert - secret: - secretName: root-ca-cert - - name: natter-token-database-cert - secret: - secretName: natter-token-database-service-cert - defaultMode: 288 - containers: - - name: natter-token-database - image: apisecurityinaction/tokendatabase:tls - imagePullPolicy: Never - volumeMounts: - - name: root-ca-cert - mountPath: "/etc/certs/root-ca" - readOnly: true - - name: natter-token-database-cert - mountPath: "/etc/certs/natter-token-database" - readOnly: true - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - all - ports: - - containerPort: 9092 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-token-database-service.yaml b/natter-api/kubernetes/natter-token-database-service.yaml deleted file mode 100644 index 5dbf1bd..0000000 --- a/natter-api/kubernetes/natter-token-database-service.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: natter-token-database-service - namespace: natter-api -spec: - selector: - app: natter-token-database - ports: - - protocol: TCP - port: 9092 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-token-service-deployment.yaml b/natter-api/kubernetes/natter-token-service-deployment.yaml deleted file mode 100644 index 97f475e..0000000 --- a/natter-api/kubernetes/natter-token-service-deployment.yaml +++ /dev/null @@ -1,45 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: token-service-deployment - namespace: natter-api -spec: - selector: - matchLabels: - app: token-service - replicas: 1 - template: - metadata: - labels: - app: token-service - spec: - securityContext: - runAsNonRoot: true - fsGroup: 1000 - volumes: - - name: root-ca-cert - secret: - secretName: root-ca-cert - - name: natter-token-cert - secret: - secretName: natter-token-service-cert - defaultMode: 288 - containers: - - name: token-service - image: apisecurityinaction/token-service:latest - imagePullPolicy: Never - volumeMounts: - - name: root-ca-cert - mountPath: "/etc/certs/root-ca" - readOnly: true - - name: natter-token-cert - mountPath: "/etc/certs/natter-token-service" - readOnly: true - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - all - ports: - - containerPort: 4567 \ No newline at end of file diff --git a/natter-api/kubernetes/network/network-policy-token-database.yaml b/natter-api/kubernetes/network/network-policy-link-preview.yaml similarity index 68% rename from natter-api/kubernetes/network/network-policy-token-database.yaml rename to natter-api/kubernetes/network/network-policy-link-preview.yaml index 639e252..608f97d 100644 --- a/natter-api/kubernetes/network/network-policy-token-database.yaml +++ b/natter-api/kubernetes/network/network-policy-link-preview.yaml @@ -1,12 +1,12 @@ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: token-database-network-policy + name: token-api-network-policy namespace: natter-api spec: podSelector: matchLabels: - app: natter-token-database + app: natter-api policyTypes: - Ingress - Egress @@ -14,7 +14,9 @@ spec: - from: - podSelector: matchLabels: - app: token-service + app: natter-api ports: - protocol: TCP - port: 9092 + port: 4567 + egress: + - to: {} diff --git a/natter-api/kubernetes/network/network-policy-token-service.yaml b/natter-api/kubernetes/network/network-policy-token-service.yaml deleted file mode 100644 index 3b6538f..0000000 --- a/natter-api/kubernetes/network/network-policy-token-service.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: token-service-network-policy - namespace: natter-api -spec: - podSelector: - matchLabels: - app: token-service - policyTypes: - - Ingress - - Egress - ingress: - - from: - - podSelector: - matchLabels: - app: natter-api - ports: - - protocol: TCP - port: 4567 - egress: - - to: - - podSelector: - matchLabels: - app: natter-token-database - ports: - - protocol: TCP - port: 9092 diff --git a/natter-api/pom.xml b/natter-api/pom.xml index 5e08de6..103ea05 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -77,6 +77,12 @@ drools-compiler ${drools.version} + + + org.jsoup + jsoup + 1.12.1 + @@ -96,11 +102,6 @@ ${exec.mainClass} -Djava.security.egd=file:/dev/urandom - -Djavax.net.ssl.trustStore=/etc/certs/root-ca/root-ca.p12 - -Djavax.net.ssl.trustStorePassword=changeit - -Djavax.net.ssl.keyStore=${keystore.path} - -Djavax.net.ssl.keyStorePassword=changeit - -Dh2.enableAnonymousTLS=false 4567 diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java b/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java new file mode 100644 index 0000000..ea7e4f0 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java @@ -0,0 +1,53 @@ +package com.manning.apisecurityinaction; + +import java.net.*; + +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.slf4j.*; +import spark.ExceptionHandler; + +import static spark.Spark.*; + +public class LinkPreviewService { + private static final Logger logger = + LoggerFactory.getLogger(LinkPreviewService.class); + + public static void main(String...args) { + afterAfter((request, response) -> { + response.type("application/json; charset=utf-8"); + }); + + get("/preview", (request, response) -> { + var url = request.queryParams("url"); + var doc = Jsoup.connect(url).timeout(3000).get(); + var title = doc.title(); + var desc = doc.head() + .selectFirst("meta[property='og:description']"); + var img = doc.head() + .selectFirst("meta[property='og:image']"); + + return new JSONObject() + .put("url", doc.location()) + .putOpt("title", title) + .putOpt("description", + desc == null ? null : desc.attr("content")) + .putOpt("image", + img == null ? null : img.attr("content")); + }); + + exception(IllegalArgumentException.class, handleException(400)); + exception(MalformedURLException.class, handleException(400)); + exception(Exception.class, handleException(502)); + exception(UnknownHostException.class, handleException(404)); + } + + private static ExceptionHandler + handleException(int status) { + return (ex, request, response) -> { + logger.error("Caught error {} - returning status {}", ex, status); + response.status(status); + response.body(new JSONObject().put("status", status).toString()); + }; + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index e27e99c..f5a115f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -1,6 +1,7 @@ package com.manning.apisecurityinaction; -import java.net.URI; +import java.io.FileInputStream; +import java.security.KeyStore; import java.sql.Connection; import java.util.Set; @@ -25,12 +26,10 @@ public static void main(String... args) throws Exception { EmbeddedServers.add(EmbeddedServers.defaultIdentifier(), new EmbeddedJettyFactory().withHttpOnly(true)); Spark.staticFiles.location("/public"); - secure("/etc/certs/natter-api/natter-api-service.p12", - "changeit", null, null); port(args.length > 0 ? Integer.parseInt(args[0]) : SPARK_DEFAULT_PORT); - var jdbcUrl = "jdbc:h2:ssl://natter-database-service:9092/mem:natter"; + var jdbcUrl = "jdbc:h2:tcp://natter-database-service:9092/mem:natter"; var datasource = JdbcConnectionPool.create( jdbcUrl, "natter", "password"); createTables(datasource.getConnection()); @@ -38,10 +37,15 @@ public static void main(String... args) throws Exception { jdbcUrl, "natter_api_user", "password"); var database = Database.forDataSource(datasource); - SecureTokenStore tokenStore = new RemoteTokenStore( - "https://natter-token-service:4567/tokens"); + var keystore = KeyStore.getInstance("PKCS12"); + keystore.load(new FileInputStream("keystore.p12"), + "changeit".toCharArray()); + var macKey = keystore.getKey("hmac-key", "changeit".toCharArray()); + + SecureTokenStore tokenStore = new MacaroonTokenStore( + new JsonTokenStore(), macKey); var capController = new CapabilityController(tokenStore); - var tokenController = new TokenController(tokenStore); + var tokenController = new TokenController(new CookieTokenStore()); var spaceController = new SpaceController(database, capController); var userController = new UserController(database); @@ -62,16 +66,6 @@ public static void main(String... args) throws Exception { } })); - - var header = new JSONObject() - .put("alg", "HS256") - .put("typ", "JWT"); - - var clientId = "testClient"; - var clientSecret = "60ho9IS3d6/A+Zzvdn9Y4laiGnI/1TddTM95lEHjArw="; - var introspectionEndpoint = - URI.create("https://as.example.com:8443/oauth2/introspect"); - before(userController::authenticate); before(tokenController::validateToken); @@ -91,6 +85,7 @@ public static void main(String... args) throws Exception { post("/users", userController::registerUser); before("/spaces", userController::requireAuthentication); + before("/spaces", userController::lookupPermissions); before("/spaces", tokenController.requireScope("POST", "create_space")); post("/spaces", spaceController::createSpace); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index 1a9e937..5a0a344 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -1,8 +1,13 @@ package com.manning.apisecurityinaction.controller; +import java.net.*; +import java.net.http.*; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Set; +import java.util.*; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.dalesbred.Database; @@ -114,10 +119,42 @@ public Message readMessage(Request request, Response response) { "FROM messages WHERE msg_id = ? AND space_id = ?", msgId, spaceId); + var linkPattern = Pattern.compile("https?://\\S+"); + var matcher = linkPattern.matcher(message.message); + int start = 0; + while (matcher.find(start)) { + var url = matcher.group(); + var preview = fetchLinkPreview(url); + if (preview != null) { + message.links.add(preview); + } + start = matcher.end(); + } + response.status(200); return message; } + private final HttpClient httpClient = HttpClient.newHttpClient(); + private final URI linkPreviewService = URI.create( + "http://natter-link-preview-service:4567"); + + private JSONObject fetchLinkPreview(String link) { + var url = linkPreviewService.resolve("/preview?url=" + + URLEncoder.encode(link, StandardCharsets.UTF_8)); + var request = HttpRequest.newBuilder(url) + .GET() + .build(); + try { + var response = httpClient.send(request, + BodyHandlers.ofString()); + if (response.statusCode() == 200) { + return new JSONObject(response.body()); + } + } catch (Exception ignored) { } + return null; + } + public JSONArray findMessages(Request request, Response response) { var since = Instant.now().minus(1, ChronoUnit.DAYS); if (request.queryParams("since") != null) { @@ -170,6 +207,7 @@ public static class Message { private final String author; private final Instant time; private final String message; + private final List links = new ArrayList<>(); public Message(long spaceId, long msgId, String author, Instant time, String message) { @@ -187,6 +225,7 @@ public String toString() { msg.put("author", author); msg.put("time", time.toString()); msg.put("message", message); + msg.put("links", links); return msg.toString(); } } From aaac44fb2b30783cd283744927a2e65664a63880 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 21 Nov 2019 21:27:46 +0000 Subject: [PATCH 159/209] Avoid SSRF and DNS rebinding attacks --- .../network/network-policy-natter-api.yaml | 2 +- .../LinkPreviewService.java | 60 ++++++++++++++++++- .../com/manning/apisecurityinaction/Main.java | 17 ++++-- .../controller/CapabilityController.java | 21 +++---- .../controller/SpaceController.java | 11 +--- 5 files changed, 83 insertions(+), 28 deletions(-) diff --git a/natter-api/kubernetes/network/network-policy-natter-api.yaml b/natter-api/kubernetes/network/network-policy-natter-api.yaml index c2d7c87..8c167ed 100644 --- a/natter-api/kubernetes/network/network-policy-natter-api.yaml +++ b/natter-api/kubernetes/network/network-policy-natter-api.yaml @@ -25,7 +25,7 @@ spec: - to: - podSelector: matchLabels: - app: token-service + app: link-preview-service ports: - protocol: TCP port: 4567 diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java b/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java index ea7e4f0..93d3683 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java @@ -1,12 +1,16 @@ package com.manning.apisecurityinaction; +import java.io.IOException; import java.net.*; +import java.util.Set; import org.json.JSONObject; -import org.jsoup.Jsoup; +import org.jsoup.*; +import org.jsoup.nodes.Document; import org.slf4j.*; import spark.ExceptionHandler; +import static org.jsoup.Connection.Method.GET; import static spark.Spark.*; public class LinkPreviewService { @@ -18,9 +22,18 @@ public static void main(String...args) { response.type("application/json; charset=utf-8"); }); + var expectedHostNames = Set.of( + "natter-link-preview-service:4567", + "natter-link-preview-service.natter-api:4567"); + before((request, response) -> { + if (!expectedHostNames.contains(request.host())) { + halt(400); + } + }); + get("/preview", (request, response) -> { var url = request.queryParams("url"); - var doc = Jsoup.connect(url).timeout(3000).get(); + var doc = fetch(url); var title = doc.title(); var desc = doc.head() .selectFirst("meta[property='og:description']"); @@ -42,6 +55,49 @@ public static void main(String...args) { exception(UnknownHostException.class, handleException(404)); } + private static Document fetch(String url) throws IOException { + Document doc = null; + var retries = 0; + while (doc == null && retries++ < 10) { + logger.info("Checking URL {}", url); + if (isBlockedAddress(url)) { + throw new IllegalArgumentException( + "URL refers to local/private address"); + } + var res = Jsoup.connect(url).followRedirects(false) + .timeout(3000).method(GET).execute(); + if (res.statusCode() / 100 == 3) { + url = res.header("Location"); + } else { + doc = res.parse(); + } + } + if (doc == null) throw new IOException("too many redirects"); + return doc; + } + + private static boolean isBlockedAddress(String uri) + throws UnknownHostException { + var host = URI.create(uri).getHost(); + for (var ipAddr : InetAddress.getAllByName(host)) { + if (ipAddr.isLoopbackAddress() || + ipAddr.isLinkLocalAddress() || + ipAddr.isSiteLocalAddress() || + ipAddr.isMulticastAddress() || + ipAddr.isAnyLocalAddress() || + isUniqueLocalAddress(ipAddr)) { + return true; + } + } + return false; + } + + private static boolean isUniqueLocalAddress(InetAddress ipAddr) { + return ipAddr instanceof Inet6Address && + (ipAddr.getAddress()[0] & 0xFF) == 0xFD && + (ipAddr.getAddress()[1] & 0xFF) == 0X00; + } + private static ExceptionHandler handleException(int status) { return (ex, request, response) -> { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index f5a115f..e63a7cc 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -42,10 +42,10 @@ public static void main(String... args) throws Exception { "changeit".toCharArray()); var macKey = keystore.getKey("hmac-key", "changeit".toCharArray()); - SecureTokenStore tokenStore = new MacaroonTokenStore( - new JsonTokenStore(), macKey); + SecureTokenStore tokenStore = new HmacTokenStore( + new DatabaseTokenStore(database), macKey); var capController = new CapabilityController(tokenStore); - var tokenController = new TokenController(new CookieTokenStore()); + var tokenController = new TokenController(tokenStore); var spaceController = new SpaceController(database, capController); var userController = new UserController(database); @@ -57,6 +57,16 @@ public static void main(String... args) throws Exception { }); before(new CorsFilter(Set.of("https://localhost:9999"))); + var expectedHostNames = Set.of( + "natter-api-service:4567", + "natter-api-service.natter-api:4567" + ); + before((request, response) -> { + if (!expectedHostNames.contains(request.host())) { + halt(400); + } + }); + before(((request, response) -> { if (request.requestMethod().equals("POST") && !"application/json".equals(request.contentType())) { @@ -85,7 +95,6 @@ public static void main(String... args) throws Exception { post("/users", userController::registerUser); before("/spaces", userController::requireAuthentication); - before("/spaces", userController::lookupPermissions); before("/spaces", tokenController.requireScope("POST", "create_space")); post("/spaces", spaceController::createSpace); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java index 5a0a2c3..ab58008 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java @@ -1,6 +1,6 @@ package com.manning.apisecurityinaction.controller; -import java.net.*; +import java.net.URI; import java.time.Instant; import java.util.Objects; @@ -8,7 +8,11 @@ import com.manning.apisecurityinaction.token.TokenStore.Token; import spark.*; +import static java.time.temporal.ChronoUnit.DAYS; + public class CapabilityController { + private static final Instant NON_EXPIRING = + Instant.EPOCH.plus(10000 * 365, DAYS); private final SecureTokenStore tokenStore; @@ -17,26 +21,19 @@ public CapabilityController(SecureTokenStore tokenStore) { } public URI createUri(Request request, String path, String perms) { - var token = new Token(Instant.MAX, null); + var token = new Token(NON_EXPIRING, null); token.attributes.put("path", path); token.attributes.put("perms", perms); var tokenId = tokenStore.create(request, token); var base = URI.create(request.url()); - try { - return new URI(base.getScheme(), tokenId, base.getHost(), - base.getPort(), path, null, null); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } + return base.resolve(path + "?access_token=" + tokenId); } public void lookupPermissions(Request request, Response response) { - var authHeader = request.headers("Authorization"); - if (authHeader == null || !authHeader.startsWith("Bearer ")) - return; - var tokenId = authHeader.substring(7).trim(); + var tokenId = request.queryParams("access_token"); + if (tokenId == null) return; tokenStore.read(request, tokenId).ifPresent(token -> { var tokenPath = token.attributes.get("path"); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java index 5a0a344..6562947 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java @@ -115,7 +115,7 @@ public Message readMessage(Request request, Response response) { var msgId = Long.parseLong(request.params(":msgId")); var message = database.findUnique(Message.class, - "SELECT space_id, msg_id, author, msg_time, msg_text " + + "SELECT author, msg_time, msg_text " + "FROM messages WHERE msg_id = ? AND space_id = ?", msgId, spaceId); @@ -202,17 +202,12 @@ public JSONObject addMember(Request request, Response response) { } public static class Message { - private final long spaceId; - private final long msgId; private final String author; private final Instant time; private final String message; private final List links = new ArrayList<>(); - public Message(long spaceId, long msgId, String author, - Instant time, String message) { - this.spaceId = spaceId; - this.msgId = msgId; + public Message(String author, Instant time, String message) { this.author = author; this.time = time; this.message = message; @@ -220,8 +215,6 @@ public Message(long spaceId, long msgId, String author, @Override public String toString() { JSONObject msg = new JSONObject(); - msg.put("uri", - "/spaces/" + spaceId + "/messages/" + msgId); msg.put("author", author); msg.put("time", time.toString()); msg.put("message", message); From ac90504e91b01f1b5b1f04031e02296e98c29911 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 21 Nov 2019 21:32:53 +0000 Subject: [PATCH 160/209] Delete network policies --- .../network/network-policy-default-deny.yaml | 10 ------ .../network/network-policy-link-preview.yaml | 22 ------------- .../network/network-policy-natter-api.yaml | 31 ------------------- .../network-policy-natter-database.yaml | 20 ------------ 4 files changed, 83 deletions(-) delete mode 100644 natter-api/kubernetes/network/network-policy-default-deny.yaml delete mode 100644 natter-api/kubernetes/network/network-policy-link-preview.yaml delete mode 100644 natter-api/kubernetes/network/network-policy-natter-api.yaml delete mode 100644 natter-api/kubernetes/network/network-policy-natter-database.yaml diff --git a/natter-api/kubernetes/network/network-policy-default-deny.yaml b/natter-api/kubernetes/network/network-policy-default-deny.yaml deleted file mode 100644 index db922d4..0000000 --- a/natter-api/kubernetes/network/network-policy-default-deny.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: default-deny-network-policy - namespace: natter-api -spec: - podSelector: {} - policyTypes: - - Ingress - - Egress \ No newline at end of file diff --git a/natter-api/kubernetes/network/network-policy-link-preview.yaml b/natter-api/kubernetes/network/network-policy-link-preview.yaml deleted file mode 100644 index 608f97d..0000000 --- a/natter-api/kubernetes/network/network-policy-link-preview.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: token-api-network-policy - namespace: natter-api -spec: - podSelector: - matchLabels: - app: natter-api - policyTypes: - - Ingress - - Egress - ingress: - - from: - - podSelector: - matchLabels: - app: natter-api - ports: - - protocol: TCP - port: 4567 - egress: - - to: {} diff --git a/natter-api/kubernetes/network/network-policy-natter-api.yaml b/natter-api/kubernetes/network/network-policy-natter-api.yaml deleted file mode 100644 index 8c167ed..0000000 --- a/natter-api/kubernetes/network/network-policy-natter-api.yaml +++ /dev/null @@ -1,31 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: token-api-network-policy - namespace: natter-api -spec: - podSelector: - matchLabels: - app: natter-api - policyTypes: - - Ingress - - Egress - ingress: - - ports: - - protocol: TCP - port: 4567 - egress: - - to: - - podSelector: - matchLabels: - app: natter-database - ports: - - protocol: TCP - port: 9092 - - to: - - podSelector: - matchLabels: - app: link-preview-service - ports: - - protocol: TCP - port: 4567 diff --git a/natter-api/kubernetes/network/network-policy-natter-database.yaml b/natter-api/kubernetes/network/network-policy-natter-database.yaml deleted file mode 100644 index b557b19..0000000 --- a/natter-api/kubernetes/network/network-policy-natter-database.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: natter-database-network-policy - namespace: natter-api -spec: - podSelector: - matchLabels: - app: natter-database - policyTypes: - - Ingress - - Egress - ingress: - - from: - - podSelector: - matchLabels: - app: natter-api - ports: - - protocol: TCP - port: 9092 From 87592c32f1d77bf3a8f605e75fbf2e0dd987c5b5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 22 Nov 2019 11:07:30 +0000 Subject: [PATCH 161/209] Enable Linkerd service mesh --- natter-api/kubernetes/natter-namespace.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/natter-api/kubernetes/natter-namespace.yaml b/natter-api/kubernetes/natter-namespace.yaml index 4faa403..6846664 100644 --- a/natter-api/kubernetes/natter-namespace.yaml +++ b/natter-api/kubernetes/natter-namespace.yaml @@ -4,3 +4,5 @@ metadata: name: natter-api labels: name: natter-api + annotations: + linkerd.io/inject: enabled From 09e1fc07578dfaaea68c6d62895f5e38053a86ac Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 22 Nov 2019 17:10:52 +0000 Subject: [PATCH 162/209] Update valid host headers --- natter-api/root-ca.p12 | Bin 1506 -> 0 bytes .../com/manning/apisecurityinaction/Main.java | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 natter-api/root-ca.p12 diff --git a/natter-api/root-ca.p12 b/natter-api/root-ca.p12 deleted file mode 100644 index 3624f145ad39af792cd69bc28f9db41f7a991ff1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1506 zcmV<81s(b@f(70J0Ru3C1(ya1Duzgg_YDCD0ic2fhy;QKgfM~yfG~mud=mju=P9@RHwKdUCHgI-Qsg zoxV2x!FrZT^6rQm%rcz%BQ@7jB_8uQMZ55ofp#N8If!440p{j4RnTVeAUYusls7yF z?Cb4T3#i$JYETAz3SD)1GB(G~jZ-!CI@^B@(JbABW%5tj|A!MNKUKQ_ASyB*Gq0fW zY97ROOiX!COm(^#;*$|9KBk>#4X$B1fgvwo!hf-t}!x_f-1 z2DVj6(ey+W9C-n_7SnnyFgS4rLBPUINB8SJ=usYtNM2So%;?q>Hv)o|t>Nyggq7>MB4kpO3~`1+ADMynhyB+K{@w8+g8&%JIME#oP~Hl~gv z{1f-hX+hZ`z?TPvDqOLe_AL`Nx_#TOu$XM0%3yK^;`RTd=<8*+`G(qG*_%^NtrDpo?Y=t{;wn<$A{CIWL4r40FtGC3Nx^c8! z3-YN@u3Xl_uMwWS8i{SqYuO}z%0GHZiAC7nL3JN)6c>eOa$h}}^lz$n5^x(#{KdZc zUGW75>z&*t!vX?-)CuV%{>n5izkUXfq_;+Z^rM5&`9#&DP;-Y(i;!1WefH0!?sd1wjBYQ^}PLZ zo;Dft#bZ@5;|q3Msw~8G!_ad?WrzNoJh>s^6<4ew>;lcgXAR}S#nycYg7Azf$U;Wk z7Fh`b?ULZkL00~=N8uUHHfqn@)|eTUhw#_+{60-Ptf7iQp250rWsq zSXp4FQ6(VHtVLdY^moi6_?sVpt2<(kdA)W+!naER$V|01d%tMylnhyPcsK*!IFA{v9NR!9Zn zIFMxc&<&9RhN2P1m94GpFAavMMdg;7qIjobO^b-~EjZysu>)kOfRxoVdE*)2#)Gc} zzxRsJ`t*6_26s=is?ssWlm}f$F~!+WA-|mGo**G}>RkA1kOn z?$Lw)9U3U1!NYt7okgLQJ^+P^3I>u2$Lr1(8M;71?R@!bDVt*Y`8+kP(oBcE3LSpr zs*e-odw`~(GIDo{@3ev41}1{d%)TFVz|G`L6T+e`k(8RAt#KxC_~LR|P&Nb?p6|U7 z=3lw?W^(s@=g1)CX78VwmTYA8cbt)??LxDJGE-z@ zBt_zCAGz^VtVHAjleP<{rHPK%9zl!Ivc~AV$M-h`T?C$APGi3k-}It+PHVnMF|h9=1`ln z$ZroFw}OALE_wKFcJRaS>R2afd^f=F`!AEG%XBmYb;J4*v)96V#c+_Mi0$HvTKy_t z!44Edi|Jz9kll6J@c!x{TxZ_4!3v4WM0;7jLgW|FR*R#29D;DcD@*JYZsg4bt97~X zYTtW&1D?nGjESY6c2-u~6}S1L98szK;v==r`OV diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index e63a7cc..78e97bc 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -59,7 +59,9 @@ public static void main(String... args) throws Exception { var expectedHostNames = Set.of( "natter-api-service:4567", - "natter-api-service.natter-api:4567" + "natter-api-service.natter-api:4567", + "natter-api-service.natter-api.svc.cluster.local:4567", + "192.168.99.116:30567" ); before((request, response) -> { if (!expectedHostNames.contains(request.host())) { From af4836b58b5edbeb7522f07ff20e99ebe4d14a0d Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 25 Nov 2019 16:16:05 +0000 Subject: [PATCH 163/209] Add ingress controller --- natter-api/kubernetes/natter-ingress.yaml | 20 +++++++++++++++++++ .../com/manning/apisecurityinaction/Main.java | 5 +++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 natter-api/kubernetes/natter-ingress.yaml diff --git a/natter-api/kubernetes/natter-ingress.yaml b/natter-api/kubernetes/natter-ingress.yaml new file mode 100644 index 0000000..a801b4f --- /dev/null +++ b/natter-api/kubernetes/natter-ingress.yaml @@ -0,0 +1,20 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: api-ingress + namespace: natter-api + annotations: + nginx.ingress.kubernetes.io/upstream-vhost: + "$service_name.$namespace.svc.cluster.local:$service_port" +spec: + tls: + - hosts: + - api.natter.local + secretName: natter-tls + rules: + - host: api.natter.local + http: + paths: + - backend: + serviceName: natter-api-service + servicePort: 4567 \ No newline at end of file diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 78e97bc..3816b98 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -58,10 +58,11 @@ public static void main(String... args) throws Exception { before(new CorsFilter(Set.of("https://localhost:9999"))); var expectedHostNames = Set.of( + "api.natter.local", + "api.natter.local:30567", "natter-api-service:4567", "natter-api-service.natter-api:4567", - "natter-api-service.natter-api.svc.cluster.local:4567", - "192.168.99.116:30567" + "natter-api-service.natter-api.svc.cluster.local:4567" ); before((request, response) -> { if (!expectedHostNames.contains(request.host())) { From b4d11a5b7cad76525473fc5d603af046cb11e70d Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 25 Nov 2019 16:17:11 +0000 Subject: [PATCH 164/209] Add sample private key and certificate PEMs --- natter-api/api.natter.local-key.pem | 28 ++++++++++++++++++++++++++++ natter-api/api.natter.local.pem | 26 ++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 natter-api/api.natter.local-key.pem create mode 100644 natter-api/api.natter.local.pem diff --git a/natter-api/api.natter.local-key.pem b/natter-api/api.natter.local-key.pem new file mode 100644 index 0000000..912de37 --- /dev/null +++ b/natter-api/api.natter.local-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC9SSZNB9OPTnCa +2/w/jnbm9MERUkuTSeCRW+s2YbHu5P9s1sizyZ54qaFpqmzUiYc+u9PuhBNctfP2 +N4FdOnNjcZCh7xaHTiUIm4DERLkJOvokTjTQv4myZ4N9ePZeKIE41CKKUeYF/llQ +gCtR4E9BYFzkRyDBy/FVYweunCXKuG0zvfvOhTTtvOCgBZEjRVdO/53qOEcn5ELn +NjEpG2QpepXDuTwCmruUkVNqPppEckyknRxV4/LVaS3zTMEwrnDH172ky3GGOiZD +3zU5gUqNM7DaIZd2WuqGJk9HW2kNhG8U9Htaiijeb/PlDAMmAbeTWguvWzbwPCo3 +74W5TdhdAgMBAAECggEBAK9FpeacQaUoQBLVct0zQRyZNJGif4KyXPSchc/EZOvO +NkqFFDGOl2QpxuI+QioH8yj+6b6po/gsL+wk92/paOGDTib0agr+LEKtI24aKLDI +YMnvdO56/bkqKtKriI4luYpyvE0SiwmvvOpS1Eorh5gE798dkdKB00V6vqlLw57S +z9B5y9jpsg+CRMQuxfFL24CJXlnFFg9WpD8GNTlKTQkp/c88VRmZXmcc4sioPdV3 +GqekRrqTlpcRdvws1aOPN7DiKY9LSdzlaaUOH30T2iJ28DPwe5Zg44tuFuvwn3Wi +ssA2ZlwDdvPhXEI9kwH7cQzelmwCIGboSZHtUkaPAw0CgYEA8/Kh56OAgFfpVr1k +0WrGWm2GvYxQslXtrspG6l4648hOzDrKUh69nPH/r4nmzy+ODKcKVj3ckeAioXEb +Ss/a4mv+KOlulJK7nt+/thz338qIdBZmJDrDT9YLo7GtKUb5tbxgzKGd9Cgi2SGA +Nd+KiYgobJEPddOmuQyxgEFIwQMCgYEAxqMrpMm+7EotvvgSkWA6mBLc7EiUBq8s +qEdlb5IX+kizCoQLHOizTTa3pjlIFfHG6w5jeoQHl9rfKVh+eep36HLiMpC6GY7B +U52L2uAytGsdDuoGYZdxpX6PM2UnJDCNYUoPdIwjx9f8EQQ95uIqGnkHQycEePd1 +P6a0qn0j0x8CgYBADKlzvxsDF5HdQ1bQIR+5KF6jL88UM7l3FgbujBUcL0B5IMp0 +KzwPk/5U4XknVs4OBmGRaSabamTNTHwk9VP79OzDYx60hZ4bRZX5Q7vVF0EicasZ +wg/7yzA9J25WkxsHG1GzCJAHRe54YfJesrWWDJjIgIG1pv90QJ/uE7X9bwKBgQC8 +wa+mf0Qbe/3unAPg+6WSf1JKgkmP9ISmQHpGxHhekRj6JDH/Pa2s8RMhNQuoNsHE ++j5T3QTuK8Gmo35EUiexzwHd9SOzR7G0yGBvFF96jNLnKkH4GRaYoiRoPXYtcKnY +yqzXHpidvkO80+AS99X0pA/fo0MfxF85pivGWvZhFwKBgQDzl159KnPOm+bNgQtj +ofl3LIErQLorY3BzvSESo8YJ75RbOQfKqCS/QS7M5eI35HKPz7mG/w/e1aW5+N+D +bheLxNWKdV3suyV/fdkgVMXnzu/qEf62uDuOGqYtin8sMITc4fda+Su2cC2MwiLN +84KjzkwozDSLAyEa9tWKqY/4rA== +-----END PRIVATE KEY----- diff --git a/natter-api/api.natter.local.pem b/natter-api/api.natter.local.pem new file mode 100644 index 0000000..4dd6bcb --- /dev/null +++ b/natter-api/api.natter.local.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEVzCCAr+gAwIBAgIRAIg4YhERsteD+T8/o4gLbGkwDQYJKoZIhvcNAQELBQAw +eTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMScwJQYDVQQLDB5uZWls +QGd1ZXN0MnMtTWFjQm9vay1Qcm8ubG9jYWwxLjAsBgNVBAMMJW1rY2VydCBuZWls +QGd1ZXN0MnMtTWFjQm9vay1Qcm8ubG9jYWwwHhcNMTkwNjAxMDAwMDAwWhcNMjkx +MTI0MTQ1ODQwWjBgMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlm +aWNhdGUxNTAzBgNVBAsMLG5laWxAZ3Vlc3Qycy1NYWNCb29rLVByby5sb2NhbCAo +TmVpbCBNYWRkZW4pMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvUkm +TQfTj05wmtv8P4525vTBEVJLk0ngkVvrNmGx7uT/bNbIs8meeKmhaaps1ImHPrvT +7oQTXLXz9jeBXTpzY3GQoe8Wh04lCJuAxES5CTr6JE400L+JsmeDfXj2XiiBONQi +ilHmBf5ZUIArUeBPQWBc5EcgwcvxVWMHrpwlyrhtM737zoU07bzgoAWRI0VXTv+d +6jhHJ+RC5zYxKRtkKXqVw7k8Apq7lJFTaj6aRHJMpJ0cVePy1Wkt80zBMK5wx9e9 +pMtxhjomQ981OYFKjTOw2iGXdlrqhiZPR1tpDYRvFPR7Wooo3m/z5QwDJgG3k1oL +r1s28DwqN++FuU3YXQIDAQABo3MwcTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAww +CgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBTXxvVMeFvupdtt +nvRQq/5lEN7kLjAbBgNVHREEFDASghBhcGkubmF0dGVyLmxvY2FsMA0GCSqGSIb3 +DQEBCwUAA4IBgQBfM8/BqK/3Lk3cNX4CJFVgy6v07zS2QPLUpPzisJ/DTMa1NPgB +My6oSUM52DNH87hjtkxR33j4rBriOQ3lOnDpbUahYHLNDyvfx4Waq0+kkkk5IXbI +eXrPN1kjUM55xOx1lh95N0elUn2zsofnMBqUbGcCk2q36EgA9yXLRYjlfq9fV+vY +1NvvMF7jjQRPgQ8+5N+dmOudWnDR+ADY0epSYUqQtHqNVDlbR8wS3q8Ws8gvweUD +4QhOpVkDLpdoqHxRuPW8U/EkcH8NuskTeTP9CmoQfzqmUAG1BqM6umm4r8uhRabh +7SpYuwU94EmoHK8+VLAAIM9VmNHRfqkKgOiQhk8/Ayv9csR9KrIt/i+yKCDERy5l +9U0yyXMI7FD1zbraBLhNweWlXdTcQKim2BXoYqg7wjgay770m/9ktD+aYyoPCmI6 +0dS/dMsWaD6S7im1jgOMtEr7hnFr7xRopTUett57Zn8mw477IhJgRzI85ZkoIhnJ +ZTYUGqjkdP1N/s8= +-----END CERTIFICATE----- From b8b9027161c18aa95461b40c669b9975851e98a1 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 27 Nov 2019 16:50:49 +0000 Subject: [PATCH 165/209] Update README for chapter 10 --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 485f7f3..ed2e94d 100644 --- a/README.md +++ b/README.md @@ -79,3 +79,8 @@ descriptions for HTTP requests that can be - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter09) - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter09-end) + +### Chapter 10 - Microservice APIs in Kubernetes + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter10) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter10-end) From 92d4090613b22359e6c2f414c823efddf660abc4 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 27 Nov 2019 17:09:15 +0000 Subject: [PATCH 166/209] Use password component for capability token --- .../apisecurityinaction/controller/CapabilityController.java | 3 ++- natter-api/src/main/resources/public/capability.html | 2 +- natter-api/src/main/resources/public/capability.js | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java index 5a0a2c3..5d4774f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java @@ -25,7 +25,8 @@ public URI createUri(Request request, String path, String perms) { var base = URI.create(request.url()); try { - return new URI(base.getScheme(), tokenId, base.getHost(), + return new URI(base.getScheme(), ":" + tokenId, + base.getHost(), base.getPort(), path, null, null); } catch (URISyntaxException e) { throw new RuntimeException(e); diff --git a/natter-api/src/main/resources/public/capability.html b/natter-api/src/main/resources/public/capability.html index c4fd1f7..fe3822d 100644 --- a/natter-api/src/main/resources/public/capability.html +++ b/natter-api/src/main/resources/public/capability.html @@ -6,7 +6,7 @@

Natter

- Load messages diff --git a/natter-api/src/main/resources/public/capability.js b/natter-api/src/main/resources/public/capability.js index 3895274..ed19e9d 100644 --- a/natter-api/src/main/resources/public/capability.js +++ b/natter-api/src/main/resources/public/capability.js @@ -1,7 +1,8 @@ function getCap(url, callback) { let capUrl = new URL(url); - let token = capUrl.username; + let token = capUrl.password; capUrl.username = ''; + capUrl.password = ''; return fetch(capUrl.href, { headers: { From 79ba2224d0f11b2a974ba86480567292a5f1f506 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 14 Dec 2019 17:00:47 +0000 Subject: [PATCH 167/209] Increase visibility of sha256 method for reuse --- .../manning/apisecurityinaction/token/CookieTokenStore.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java index e82bb4e..bf06708 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java @@ -62,10 +62,11 @@ public void revoke(Request request, String tokenId) { session.invalidate(); } - private static byte[] sha256(String tokenId) { + static byte[] sha256(String tokenId) { try { var sha256 = MessageDigest.getInstance("SHA-256"); - return sha256.digest(tokenId.getBytes(StandardCharsets.UTF_8)); + return sha256.digest( + tokenId.getBytes(StandardCharsets.UTF_8)); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); } From b3db2f77d5bc79a0963dc11ef16327443a4b24e6 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 14 Dec 2019 17:48:29 +0000 Subject: [PATCH 168/209] Pull out base64 encoder field --- .../apisecurityinaction/token/DatabaseTokenStore.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java index 2e446a3..3fd7b9b 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java @@ -14,6 +14,8 @@ public class DatabaseTokenStore implements TokenStore { private static final Logger logger = LoggerFactory.getLogger(DatabaseTokenStore.class); + private final Base64.Encoder encoder = + Base64.getUrlEncoder().withoutPadding(); private final Database database; private final SecureRandom secureRandom; @@ -29,8 +31,7 @@ public DatabaseTokenStore(Database database) { private String randomId() { var bytes = new byte[20]; secureRandom.nextBytes(bytes); - return Base64.getUrlEncoder().withoutPadding() - .encodeToString(bytes); + return encoder.encodeToString(bytes); } @Override From 5253fd142a4afe84de8a414f18202a166aed390f Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sat, 14 Dec 2019 18:06:39 +0000 Subject: [PATCH 169/209] Hash tokens in the DatabaseTokenStore --- .../token/DatabaseTokenStore.java | 21 ++++++++++++------- natter-api/src/main/resources/schema.sql | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java index 3fd7b9b..906e963 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java @@ -1,14 +1,16 @@ package com.manning.apisecurityinaction.token; +import java.security.SecureRandom; +import java.sql.*; +import java.util.*; +import java.util.concurrent.*; + import org.dalesbred.Database; import org.json.JSONObject; import org.slf4j.*; import spark.Request; -import java.security.SecureRandom; -import java.sql.*; -import java.util.*; -import java.util.concurrent.*; +import static com.manning.apisecurityinaction.token.CookieTokenStore.sha256; public class DatabaseTokenStore implements TokenStore { private static final Logger logger = @@ -41,7 +43,7 @@ public String create(Request request, Token token) { database.updateUnique("INSERT INTO " + "tokens(token_id, user_id, expiry, attributes) " + - "VALUES(?, ?, ?, ?)", tokenId, token.username, + "VALUES(?, ?, ?, ?)", hash(tokenId), token.username, token.expiry, attrs); return tokenId; @@ -51,13 +53,18 @@ public String create(Request request, Token token) { public Optional read(Request request, String tokenId) { return database.findOptional(this::readToken, "SELECT user_id, expiry, attributes " + - "FROM tokens WHERE token_id = ?", tokenId); + "FROM tokens WHERE token_id = ?", hash(tokenId)); } @Override public void revoke(Request request, String tokenId) { database.update("DELETE FROM tokens WHERE token_id = ?", - tokenId); + hash(tokenId)); + } + + private String hash(String tokenId) { + var hash = sha256(tokenId); + return encoder.encodeToString(hash); } private Token readToken(ResultSet resultSet) diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 74d3e98..cad13f7 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -37,7 +37,7 @@ CREATE TABLE permissions( ); CREATE TABLE tokens( - token_id VARCHAR(30) PRIMARY KEY, + token_id VARCHAR(100) PRIMARY KEY, user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), expiry TIMESTAMP NOT NULL, attributes VARCHAR(4096) NOT NULL From ea9398510624dd2b3751a2f45d5a436f0e698756 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 16 Dec 2019 16:33:52 +0000 Subject: [PATCH 170/209] Extract Base64url utility class --- .../apisecurityinaction/token/Base64url.java | 18 ++++++++++++++++++ .../token/CookieTokenStore.java | 7 +++---- 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/Base64url.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/Base64url.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/Base64url.java new file mode 100644 index 0000000..fb2db27 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/Base64url.java @@ -0,0 +1,18 @@ +package com.manning.apisecurityinaction.token; + +import java.util.Base64; + +public class Base64url { + private static final Base64.Encoder encoder = + Base64.getUrlEncoder().withoutPadding(); + private static final Base64.Decoder decoder = + Base64.getUrlDecoder(); + + public static String encode(byte[] data) { + return encoder.encodeToString(data); + } + + public static byte[] decode(String encoded) { + return decoder.decode(encoded); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java index bf06708..5ee8ffd 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java @@ -21,8 +21,7 @@ public String create(Request request, Token token) { session.attribute("expiry", token.expiry); session.attribute("attrs", token.attributes); - return Base64.getUrlEncoder().withoutPadding() - .encodeToString(sha256(session.id())); + return Base64url.encode(sha256(session.id())); } @Override @@ -33,7 +32,7 @@ public Optional read(Request request, String tokenId) { return Optional.empty(); } - var provided = Base64.getUrlDecoder().decode(tokenId); + var provided = Base64url.decode(tokenId); var computed = sha256(session.id()); if (!MessageDigest.isEqual(computed, provided)) { @@ -52,7 +51,7 @@ public void revoke(Request request, String tokenId) { var session = request.session(false); if (session == null) return; - var provided = Base64.getUrlDecoder().decode(tokenId); + var provided = Base64url.decode(tokenId); var computed = sha256(session.id()); if (!MessageDigest.isEqual(computed, provided)) { From 7994db5a4ef671e91acf94b8fb397448ed70fc88 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 16 Dec 2019 16:46:47 +0000 Subject: [PATCH 171/209] Use Base64url utility class --- .../token/DatabaseTokenStore.java | 8 +++----- .../token/HmacTokenStore.java | 16 ++++++---------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java index 906e963..0a11f1e 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java @@ -2,7 +2,7 @@ import java.security.SecureRandom; import java.sql.*; -import java.util.*; +import java.util.Optional; import java.util.concurrent.*; import org.dalesbred.Database; @@ -16,8 +16,6 @@ public class DatabaseTokenStore implements TokenStore { private static final Logger logger = LoggerFactory.getLogger(DatabaseTokenStore.class); - private final Base64.Encoder encoder = - Base64.getUrlEncoder().withoutPadding(); private final Database database; private final SecureRandom secureRandom; @@ -33,7 +31,7 @@ public DatabaseTokenStore(Database database) { private String randomId() { var bytes = new byte[20]; secureRandom.nextBytes(bytes); - return encoder.encodeToString(bytes); + return Base64url.encode(bytes); } @Override @@ -64,7 +62,7 @@ public void revoke(Request request, String tokenId) { private String hash(String tokenId) { var hash = sha256(tokenId); - return encoder.encodeToString(hash); + return Base64url.encode(hash); } private Token readToken(ResultSet resultSet) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java index a410392..d420fdb 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java @@ -1,11 +1,11 @@ package com.manning.apisecurityinaction.token; -import spark.Request; - import javax.crypto.Mac; import java.nio.charset.StandardCharsets; import java.security.*; -import java.util.*; +import java.util.Optional; + +import spark.Request; public class HmacTokenStore implements TokenStore { @@ -22,9 +22,7 @@ public String create(Request request, Token token) { var tokenId = delegate.create(request, token); var tag = hmac(tokenId); - return tokenId + '.' + - Base64.getUrlEncoder().withoutPadding() - .encodeToString(tag); + return tokenId + '.' + Base64url.encode(tag); } private byte[] hmac(String tokenId) { @@ -46,8 +44,7 @@ public Optional read(Request request, String tokenId) { } var realTokenId = tokenId.substring(0, index); - var provided = Base64.getUrlDecoder() - .decode(tokenId.substring(index + 1)); + var provided = Base64url.decode(tokenId.substring(index + 1)); var computed = hmac(realTokenId); if (!MessageDigest.isEqual(provided, computed)) { @@ -63,8 +60,7 @@ public void revoke(Request request, String tokenId) { if (index == -1) return; var realTokenId = tokenId.substring(0, index); - var provided = Base64.getUrlDecoder() - .decode(tokenId.substring(index + 1)); + var provided = Base64url.decode(tokenId.substring(index + 1)); var computed = hmac(realTokenId); if (!MessageDigest.isEqual(provided, computed)) { From 26d5d60ca4c4053186a185cdc93147e65b6c518c Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 16 Dec 2019 16:53:13 +0000 Subject: [PATCH 172/209] More Base64url cleanup --- .../token/EncryptedTokenStore.java | 29 ++++++++----------- .../token/JsonTokenStore.java | 11 ++++--- .../token/JwtHeaderTokenStore.java | 10 +++---- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java index 354097b..2a96474 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java @@ -1,11 +1,11 @@ package com.manning.apisecurityinaction.token; -import spark.Request; - import javax.crypto.Cipher; import javax.crypto.spec.IvParameterSpec; import java.security.*; -import java.util.*; +import java.util.Optional; + +import spark.Request; import static javax.crypto.Cipher.*; @@ -14,14 +14,9 @@ public class EncryptedTokenStore implements SecureTokenStore { private final TokenStore delegate; private final Key encryptionKey; - private final Base64.Encoder encoder; - private final Base64.Decoder decoder; - public EncryptedTokenStore(TokenStore delegate, Key encryptionKey) { this.delegate = delegate; this.encryptionKey = encryptionKey; - this.encoder = Base64.getUrlEncoder().withoutPadding(); - this.decoder = Base64.getUrlDecoder(); } @Override @@ -29,10 +24,10 @@ public String create(Request request, Token token) { var tokenId = delegate.create(request, token); var nonceAndCiphertext = encrypt(encryptionKey, - decoder.decode(tokenId)); + Base64url.decode(tokenId)); - return encoder.encodeToString(nonceAndCiphertext[0]) + '.' - + encoder.encodeToString(nonceAndCiphertext[1]); + return Base64url.encode(nonceAndCiphertext[0]) + '.' + + Base64url.encode(nonceAndCiphertext[1]); } @Override @@ -40,11 +35,11 @@ public Optional read(Request request, String tokenId) { var index = tokenId.indexOf('.'); if (index == -1) { return Optional.empty(); } - var nonce = decoder.decode(tokenId.substring(0, index)); - var encrypted = decoder.decode(tokenId.substring(index + 1)); + var nonce = Base64url.decode(tokenId.substring(0, index)); + var encrypted = Base64url.decode(tokenId.substring(index + 1)); var decrypted = decrypt(encryptionKey, nonce, encrypted); - return delegate.read(request, encoder.encodeToString(decrypted)); + return delegate.read(request, Base64url.encode(decrypted)); } @Override @@ -52,11 +47,11 @@ public void revoke(Request request, String tokenId) { var index = tokenId.indexOf('.'); if (index == -1) { return; } - var nonce = decoder.decode(tokenId.substring(0, index)); - var encrypted = decoder.decode(tokenId.substring(index + 1)); + var nonce = Base64url.decode(tokenId.substring(0, index)); + var encrypted = Base64url.decode(tokenId.substring(index + 1)); var decrypted = decrypt(encryptionKey, nonce, encrypted); - delegate.revoke(request, encoder.encodeToString(decrypted)); + delegate.revoke(request, Base64url.encode(decrypted)); } static byte[][] encrypt(Key key, byte[] message) { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java index 41382dd..a4cfe09 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java @@ -1,11 +1,11 @@ package com.manning.apisecurityinaction.token; -import org.json.*; -import spark.Request; - import java.time.Instant; import java.util.*; +import org.json.*; +import spark.Request; + import static java.nio.charset.StandardCharsets.UTF_8; public class JsonTokenStore implements TokenStore { @@ -19,14 +19,13 @@ public String create(Request request, Token token) { json.put("attrs", token.attributes); var jsonBytes = json.toString().getBytes(UTF_8); - return Base64.getUrlEncoder().withoutPadding() - .encodeToString(jsonBytes); + return Base64url.encode(jsonBytes); } @Override public Optional read(Request request, String tokenId) { try { - var decoded = Base64.getUrlDecoder().decode(tokenId); + var decoded = Base64url.decode(tokenId); var json = new JSONObject(new String(decoded, UTF_8)); var expiry = Instant.ofEpochSecond(json.getInt("exp")); var username = json.getString("sub"); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java index 634bab0..74996d1 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java @@ -1,10 +1,10 @@ package com.manning.apisecurityinaction.token; +import java.util.*; + import org.json.JSONObject; import spark.Request; -import java.util.*; - import static java.nio.charset.StandardCharsets.UTF_8; public class JwtHeaderTokenStore implements TokenStore { @@ -21,8 +21,7 @@ public JwtHeaderTokenStore(TokenStore delegate, JSONObject header) { public String create(Request request, Token token) { var tokenId = delegate.create(request, token); var headerBytes = header.toString().getBytes(UTF_8); - return Base64.getUrlEncoder().withoutPadding() - .encodeToString(headerBytes) + '.' + tokenId; + return Base64url.encode(headerBytes) + '.' + tokenId; } @Override @@ -33,8 +32,7 @@ public Optional read(Request request, String tokenId) { var encodedHeader = tokenId.substring(0, index); var realTokenId = tokenId.substring(index + 1); - var decodedHeader = Base64.getUrlDecoder() - .decode(encodedHeader); + var decodedHeader = Base64url.decode(encodedHeader); var suppliedHeader = new JSONObject( new String(decodedHeader, UTF_8)); From 3d1c7bceb9752c8fc7bb69df6808b59fc9005e14 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 9 Jan 2020 10:46:49 +0000 Subject: [PATCH 173/209] Chapter 6 revisions --- natter-api/pom.xml | 7 +- .../com/manning/apisecurityinaction/Main.java | 13 ++-- ...Store.java => EncryptedJwtTokenStore.java} | 13 ++-- .../token/EncryptedTokenStore.java | 61 +++------------ .../token/SignedJwtTokenStore.java | 78 +++++++++++++++++++ 5 files changed, 108 insertions(+), 64 deletions(-) rename natter-api/src/main/java/com/manning/apisecurityinaction/token/{JwtTokenStore.java => EncryptedJwtTokenStore.java} (87%) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java diff --git a/natter-api/pom.xml b/natter-api/pom.xml index 46778f7..b8e3ed3 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -51,7 +51,12 @@ com.nimbusds nimbus-jose-jwt - 7.2.1 + 8.3 + + + software.pando.crypto + salty-coffee + 1.0.2 \ No newline at end of file diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 63d9b1d..cbccd96 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -3,8 +3,11 @@ import com.google.common.util.concurrent.RateLimiter; import com.manning.apisecurityinaction.controller.*; import com.manning.apisecurityinaction.token.*; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.crypto.*; import org.dalesbred.Database; import org.dalesbred.result.EmptyResultException; +import software.pando.crypto.nacl.SecretBox; import org.h2.jdbcx.JdbcConnectionPool; import org.json.*; import spark.*; @@ -12,6 +15,7 @@ import spark.embeddedserver.jetty.EmbeddedJettyFactory; import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import java.io.FileInputStream; import java.nio.file.*; import java.security.KeyStore; @@ -66,14 +70,11 @@ public static void main(String... args) throws Exception { var macKey = keyStore.getKey("hmac-key", keyPassword); var encKey = keyStore.getKey("aes-key", keyPassword); - var header = new JSONObject() - .put("alg", "HS256") - .put("typ", "JWT"); - var tokenWhitelist = new DatabaseTokenStore(database); - SecureTokenStore tokenStore = - new JwtTokenStore((SecretKey) encKey, tokenWhitelist); + var naclKey = SecretBox.key(encKey.getEncoded()); + var tokenStore = new EncryptedTokenStore( + new JsonTokenStore(), naclKey); var tokenController = new TokenController(tokenStore); before(userController::authenticate); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedJwtTokenStore.java similarity index 87% rename from natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java rename to natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedJwtTokenStore.java index 582b21b..c9c64f4 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedJwtTokenStore.java @@ -9,13 +9,13 @@ import java.text.ParseException; import java.util.*; -public class JwtTokenStore implements SecureTokenStore { +public class EncryptedJwtTokenStore implements SecureTokenStore { private final SecretKey encKey; private final DatabaseTokenStore tokenWhitelist; - public JwtTokenStore(SecretKey encKey, - DatabaseTokenStore tokenWhitelist) { + public EncryptedJwtTokenStore(SecretKey encKey, + DatabaseTokenStore tokenWhitelist) { this.encKey = encKey; this.tokenWhitelist = tokenWhitelist; } @@ -32,12 +32,13 @@ public String create(Request request, Token token) { .expirationTime(Date.from(token.expiry)); token.attributes.forEach(claimsBuilder::claim); - var header = new JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM); + var header = new JWEHeader(JWEAlgorithm.DIR, + EncryptionMethod.A128CBC_HS256); var jwt = new EncryptedJWT(header, claimsBuilder.build()); try { - var encryptor = new DirectEncrypter(encKey); - jwt.encrypt(encryptor); + var encrypter = new DirectEncrypter(encKey); + jwt.encrypt(encrypter); } catch (JOSEException e) { throw new RuntimeException(e); } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java index 2a96474..a34830d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java @@ -1,14 +1,11 @@ package com.manning.apisecurityinaction.token; -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import java.security.*; +import java.security.Key; import java.util.Optional; +import software.pando.crypto.nacl.SecretBox; import spark.Request; -import static javax.crypto.Cipher.*; - public class EncryptedTokenStore implements SecureTokenStore { private final TokenStore delegate; @@ -21,59 +18,21 @@ public EncryptedTokenStore(TokenStore delegate, Key encryptionKey) { @Override public String create(Request request, Token token) { - var tokenId = delegate.create(request, token); - - var nonceAndCiphertext = encrypt(encryptionKey, - Base64url.decode(tokenId)); - - return Base64url.encode(nonceAndCiphertext[0]) + '.' - + Base64url.encode(nonceAndCiphertext[1]); + var tokenId = Base64url.decode(delegate.create(request, token)); + return SecretBox.encrypt(encryptionKey, tokenId).toString(); } @Override public Optional read(Request request, String tokenId) { - var index = tokenId.indexOf('.'); - if (index == -1) { return Optional.empty(); } - - var nonce = Base64url.decode(tokenId.substring(0, index)); - var encrypted = Base64url.decode(tokenId.substring(index + 1)); - var decrypted = decrypt(encryptionKey, nonce, encrypted); - - return delegate.read(request, Base64url.encode(decrypted)); + var box = SecretBox.fromString(tokenId); + var originalTokenId = Base64url.encode(box.decrypt(encryptionKey)); + return delegate.read(request, originalTokenId); } @Override public void revoke(Request request, String tokenId) { - var index = tokenId.indexOf('.'); - if (index == -1) { return; } - - var nonce = Base64url.decode(tokenId.substring(0, index)); - var encrypted = Base64url.decode(tokenId.substring(index + 1)); - var decrypted = decrypt(encryptionKey, nonce, encrypted); - - delegate.revoke(request, Base64url.encode(decrypted)); - } - - static byte[][] encrypt(Key key, byte[] message) { - try { - var cipher = Cipher.getInstance("ChaCha20-Poly1305"); - var nonce = new byte[12]; - new SecureRandom().nextBytes(nonce); - cipher.init(ENCRYPT_MODE, key, new IvParameterSpec(nonce)); - var encrypted = cipher.doFinal(message); - return new byte[][]{nonce, encrypted}; - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } - } - - static byte[] decrypt(Key key, byte[] nonce, byte[] ciphertext) { - try { - var cipher = Cipher.getInstance("ChaCha20-Poly1305"); - cipher.init(DECRYPT_MODE, key, new IvParameterSpec(nonce)); - return cipher.doFinal(ciphertext); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } + var box = SecretBox.fromString(tokenId); + var originalTokenId = Base64url.encode(box.decrypt(encryptionKey)); + delegate.revoke(request, originalTokenId); } } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java new file mode 100644 index 0000000..43ad4c5 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java @@ -0,0 +1,78 @@ +package com.manning.apisecurityinaction.token; + +import java.text.ParseException; +import java.util.*; + +import com.nimbusds.jose.*; +import com.nimbusds.jwt.*; +import org.slf4j.*; +import spark.Request; + +public class SignedJwtTokenStore implements SecureTokenStore { + private static final Logger logger = + LoggerFactory.getLogger(SignedJwtTokenStore.class); + + private final JWSSigner signer; + private final JWSVerifier verifier; + private final JWSAlgorithm algorithm; + private final String audience; + + public SignedJwtTokenStore(JWSSigner signer, + JWSVerifier verifier, JWSAlgorithm algorithm, + String audience) { + this.signer = signer; + this.verifier = verifier; + this.algorithm = algorithm; + this.audience = audience; + } + + @Override + public String create(Request request, Token token) { + var claimsSet = new JWTClaimsSet.Builder() + .subject(token.username) + .audience(audience) + .expirationTime(Date.from(token.expiry)) + .claim("attrs", token.attributes) + .build(); + var header = new JWSHeader(algorithm); + var jwt = new SignedJWT(header, claimsSet); + try { + jwt.sign(signer); + return jwt.serialize(); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + + @Override + public Optional read(Request request, String tokenId) { + try { + var jwt = SignedJWT.parse(tokenId); + if (!jwt.verify(verifier)) { + throw new JOSEException("Invalid signature"); + } + + var claims = jwt.getJWTClaimsSet(); + if (!claims.getAudience().contains(audience)) { + throw new JOSEException("Incorrect audience"); + } + + var expiry = claims.getExpirationTime().toInstant(); + var subject = claims.getSubject(); + var token = new Token(expiry, subject); + var attrs = claims.getJSONObjectClaim("attrs"); + attrs.forEach((key, value) -> + token.attributes.put(key, (String) value)); + + return Optional.of(token); + } catch (ParseException | JOSEException e) { + logger.debug("Unable to validate JWT", e); + return Optional.empty(); + } + } + + @Override + public void revoke(Request request, String tokenId) { + // TODO + } +} From 73d78c7137d2f1c92783865abe5fa1553be56587 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 19 Jan 2020 14:11:01 +0000 Subject: [PATCH 174/209] More chapter 6 revisions --- .../com/manning/apisecurityinaction/Main.java | 26 +++---- .../token/DatabaseTokenStore.java | 2 +- .../token/HmacTokenStore.java | 10 ++- .../token/SignedJwtTokenStore.java | 7 +- .../token/UnauthenticatedEncryptionStore.java | 72 +++++++++++++++++++ 5 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/UnauthenticatedEncryptionStore.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index cbccd96..060fb71 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -1,27 +1,23 @@ package com.manning.apisecurityinaction; +import javax.crypto.SecretKey; +import java.io.FileInputStream; +import java.nio.file.*; +import java.security.KeyStore; +import java.sql.Connection; +import java.util.Set; + import com.google.common.util.concurrent.RateLimiter; import com.manning.apisecurityinaction.controller.*; import com.manning.apisecurityinaction.token.*; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.crypto.*; import org.dalesbred.Database; import org.dalesbred.result.EmptyResultException; -import software.pando.crypto.nacl.SecretBox; import org.h2.jdbcx.JdbcConnectionPool; import org.json.*; import spark.*; import spark.embeddedserver.EmbeddedServers; import spark.embeddedserver.jetty.EmbeddedJettyFactory; -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.io.FileInputStream; -import java.nio.file.*; -import java.security.KeyStore; -import java.sql.Connection; -import java.util.Set; - import static spark.Service.SPARK_DEFAULT_PORT; import static spark.Spark.*; @@ -71,11 +67,9 @@ public static void main(String... args) throws Exception { var encKey = keyStore.getKey("aes-key", keyPassword); var tokenWhitelist = new DatabaseTokenStore(database); - - var naclKey = SecretBox.key(encKey.getEncoded()); - var tokenStore = new EncryptedTokenStore( - new JsonTokenStore(), naclKey); - var tokenController = new TokenController(tokenStore); + var tokenController = new TokenController( + new EncryptedJwtTokenStore( + (SecretKey) encKey, tokenWhitelist)); before(userController::authenticate); before(tokenController::validateToken); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java index c8d7f77..f80dbc6 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java @@ -12,7 +12,7 @@ import static com.manning.apisecurityinaction.token.CookieTokenStore.sha256; -public class DatabaseTokenStore implements SecureTokenStore { +public class DatabaseTokenStore implements ConfidentialTokenStore { private static final Logger logger = LoggerFactory.getLogger(DatabaseTokenStore.class); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java index d6d034f..6e7e5e2 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java @@ -13,10 +13,18 @@ public class HmacTokenStore implements SecureTokenStore { private final TokenStore delegate; private final Key macKey; - public HmacTokenStore(TokenStore delegate, Key macKey) { + private HmacTokenStore(TokenStore delegate, Key macKey) { this.delegate = delegate; this.macKey = macKey; } + public static SecureTokenStore wrap(ConfidentialTokenStore store, + Key macKey) { + return new HmacTokenStore(store, macKey); + } + public static AuthenticatedTokenStore wrap(TokenStore store, + Key macKey) { + return new HmacTokenStore(store, macKey); + } @Override public String create(Request request, Token token) { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java index 43ad4c5..2462021 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java @@ -5,13 +5,9 @@ import com.nimbusds.jose.*; import com.nimbusds.jwt.*; -import org.slf4j.*; import spark.Request; -public class SignedJwtTokenStore implements SecureTokenStore { - private static final Logger logger = - LoggerFactory.getLogger(SignedJwtTokenStore.class); - +public class SignedJwtTokenStore implements AuthenticatedTokenStore { private final JWSSigner signer; private final JWSVerifier verifier; private final JWSAlgorithm algorithm; @@ -66,7 +62,6 @@ public Optional read(Request request, String tokenId) { return Optional.of(token); } catch (ParseException | JOSEException e) { - logger.debug("Unable to validate JWT", e); return Optional.empty(); } } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/UnauthenticatedEncryptionStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/UnauthenticatedEncryptionStore.java new file mode 100644 index 0000000..0218df5 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/UnauthenticatedEncryptionStore.java @@ -0,0 +1,72 @@ +package com.manning.apisecurityinaction.token; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import java.security.*; +import java.util.Optional; + +import spark.Request; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * This token store encrypts the contents of the token using AES in + * unauthenticated counter mode. This is provided purely as an example + * of using types to enforce security properties. You should use the + * {@link EncryptedTokenStore} or {@link EncryptedJwtTokenStore} + * instead of this store. + */ +public class UnauthenticatedEncryptionStore implements ConfidentialTokenStore { + + private final Key encKey; + private final TokenStore delegate; + + public UnauthenticatedEncryptionStore(Key encKey, TokenStore delegate) { + this.encKey = encKey; + this.delegate = delegate; + } + + @Override + public String create(Request request, Token token) { + var tokenId = delegate.create(request, token); + return encrypt(tokenId.getBytes(UTF_8)); + } + + @Override + public Optional read(Request request, String tokenId) { + return decrypt(tokenId).flatMap(tok -> delegate.read(request, tok)); + } + + @Override + public void revoke(Request request, String tokenId) { + decrypt(tokenId).ifPresent(tok -> delegate.revoke(request, tok)); + } + + private String encrypt(byte[] data) { + try { + var cipher = Cipher.getInstance("AES/CTR/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, encKey); + var ciphertext = cipher.doFinal(data); + var iv = cipher.getIV(); + return Base64url.encode(iv) + '.' + Base64url.encode(ciphertext); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + private Optional decrypt(String encrypted) { + var index = encrypted.indexOf('.'); + if (index == -1) return Optional.empty(); + var iv = Base64url.decode(encrypted.substring(0, index)); + var ciphertext = Base64url.decode(encrypted.substring(index + 1)); + try { + var cipher = Cipher.getInstance("AES/CTR/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, encKey, + new IvParameterSpec(iv)); + var plaintext = cipher.doFinal(ciphertext); + return Optional.of(new String(plaintext, UTF_8)); + } catch (GeneralSecurityException e) { + return Optional.empty(); + } + } +} From 66e27f4a00fa3c205443cc056de87fd9cbea466f Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 19 Jan 2020 14:20:56 +0000 Subject: [PATCH 175/209] Correct type error --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index ba2bb31..06420e4 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -74,7 +74,8 @@ public static void main(String... args) throws Exception { var clientSecret = "60ho9IS3d6/A+Zzvdn9Y4laiGnI/1TddTM95lEHjArw="; var introspectionEndpoint = URI.create("https://as.example.com:8443/oauth2/introspect"); - SecureTokenStore tokenStore = new DatabaseTokenStore(database); + SecureTokenStore tokenStore = HmacTokenStore.wrap( + new DatabaseTokenStore(database), macKey); var tokenController = new TokenController(tokenStore); before(userController::authenticate); From 42ec58387e57a700631927ae50f5d7137ba4de36 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 20 Jan 2020 13:41:38 +0000 Subject: [PATCH 176/209] Fix broken HmacTokenStore usage --- .../src/main/java/com/manning/apisecurityinaction/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 3816b98..54a285d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -42,7 +42,7 @@ public static void main(String... args) throws Exception { "changeit".toCharArray()); var macKey = keystore.getKey("hmac-key", "changeit".toCharArray()); - SecureTokenStore tokenStore = new HmacTokenStore( + SecureTokenStore tokenStore = HmacTokenStore.wrap( new DatabaseTokenStore(database), macKey); var capController = new CapabilityController(tokenStore); var tokenController = new TokenController(tokenStore); From 86371c68cb7277cc9080169996910d45a0731f26 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 20 Jan 2020 13:41:59 +0000 Subject: [PATCH 177/209] Sample JWT bearer client --- natter-api/keystore.p12 | Bin 755 -> 2145 bytes natter-api/pom.xml | 5 ++ .../apisecurityinaction/JwtBearerClient.java | 64 ++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/JwtBearerClient.java diff --git a/natter-api/keystore.p12 b/natter-api/keystore.p12 index 9007e6c11b68583815fceb513feab72452eed153..246dee9ad645e9da06ee542cf1132126cc14ad39 100644 GIT binary patch delta 1480 zcmey&`cS~upot@viIEvdiLr5NwRyCC=VfGPT+qb9&eFsI6lVW#(8OYlBE_P@(!`L8*Zz8y8FnXtp$q2;1t)uiS>_)i&!w=KOKdY&iIR z;X)>6hQk3Y9tZYpy8Aa-R6fwjWV0o(C9u8K}Y?$|+(fAt%O=%8<;U z%TT~z#9+!`Ho1dIq2Aom#Kgqh#L&pd#GvsU5l*)WPWiRhZRe9o9bO)P3b!~O(2Vzi zI(@;bY4J+UudE+Na&sPjVRV7j`hu&U?~en=Q`c|qRVtjcB01>!g1ccl%~O|d$e*$- zSNhH+qurlhu29{~ainOD`5m9da~QlnYAz_f}2AfbZsSSfS6A(ma6LA&%|5B%!gamYGjH`a zv^-yxA^a(O#=7|x>Ge6cb(&;j0iwvqkO;HU*6< z5wBAxl=JQrWb)q?x!`EWYuCP;ZXRuOCciq|6Be#gG%Q!@0)o(S$?`YX}SnMR5k$b>W7VnLjYwJ%a zJ$0EWzmTcp&k@hR7Qa006uE^ZKM7?7zgTVJEq_PuNvYuM9n}K6m=63gKYZatdrOdY zO5LKbr`VQD-aEo_O6c*#>0kPcS?U?pRtMWWxZyK7tdE~BIxDLvpnvbz2~j#T91g7g zx|R8`bip?DS3XHsi+(-{W!d~q(e>j7gWALI6(j57vhIJ%ueJ+mFXKOMcp_#+>XDj# z1sB2^A7(cOZaCBS`|M^dZzzAtY``PrC> zP5s!~fBD0<9SPO z3*MOyJ=_0jazGZ!y2yKxxYee!IWonFHU?^cw)|X z4-dv=M$vjA(@!p~kLtO1>-*vI{Cn>Iw!NBF9;%-cXz}f)-nwZTw?C|3b?cJQiIhVH z>^ delta 134 zcmaDT@R`-tpo!@{6C*Q_TEWJt)#lOmotKfFaX}N)OqM35sX*b0K-`NeRR@%+0t%OH zv@K_{w=+;Q;ACUf=3{1(Vr5_vSzr6OCp*#eM?r~}<~fDadK*4UHnE7*b#j#~*L?fz WMbyue8)a2xf6WcfVq#`&TL1vAnimbus-jose-jwt 8.3 + + org.bouncycastle + bcpkix-jdk15on + 1.64 + software.pando.crypto salty-coffee diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/JwtBearerClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/JwtBearerClient.java new file mode 100644 index 0000000..2f09ca7 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/JwtBearerClient.java @@ -0,0 +1,64 @@ +package com.manning.apisecurityinaction; + +import java.io.FileInputStream; +import java.net.URI; +import java.net.http.*; +import java.security.KeyStore; +import java.security.interfaces.ECPrivateKey; +import java.util.*; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.crypto.ECDSASigner; +import com.nimbusds.jose.jwk.*; +import com.nimbusds.jwt.*; + +import static java.time.Instant.now; +import static java.time.temporal.ChronoUnit.SECONDS; + +public class JwtBearerClient { + + public static void main(String... args) throws Exception { + var password = "changeit".toCharArray(); + var keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new FileInputStream("keystore.p12"), + password); + var privateKey = (ECPrivateKey) keyStore.getKey("es256-key", + password); + + var jwk = ECKey.load(keyStore, "es256-key", password); + System.out.println("JWK Set:"); + System.out.println(new JWKSet(jwk.toPublicJWK())); + + var clientId = "test"; + var as = "https://as.example.com/access_token"; + var header = new JWSHeader(JWSAlgorithm.ES256); + var claims = new JWTClaimsSet.Builder() + .subject(clientId) + .issuer(clientId) + .expirationTime(Date.from(now().plus(30, SECONDS))) + .audience(as) + .jwtID(UUID.randomUUID().toString()) + .build(); + var jwt = new SignedJWT(header, claims); + jwt.sign(new ECDSASigner(privateKey)); + var assertion = jwt.serialize(); + System.out.println("Assertion: " + assertion); + + var form = "grant_type=client_credentials&scope=a+b+c" + + "&client_assertion_type=" + + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + + "&client_assertion=" + assertion; + + var httpClient = HttpClient.newHttpClient(); + var request = HttpRequest.newBuilder() + .uri(URI.create(as)) + .header("Content-Type", + "application/x-www-form-urlencoded") + .POST(HttpRequest.BodyPublishers.ofString(form)) + .build(); + var response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString()); + System.out.println(response.statusCode()); + System.out.println(response.body()); + } +} From ae50e0dce087feff54fd0ae56931f68af6268c59 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 27 Jan 2020 14:13:58 +0000 Subject: [PATCH 178/209] Add TLS client certificate authentication --- natter-api/kubernetes/natter-ingress.yaml | 7 +++- .../controller/UserController.java | 37 ++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/natter-api/kubernetes/natter-ingress.yaml b/natter-api/kubernetes/natter-ingress.yaml index a801b4f..257ea09 100644 --- a/natter-api/kubernetes/natter-ingress.yaml +++ b/natter-api/kubernetes/natter-ingress.yaml @@ -5,7 +5,12 @@ metadata: namespace: natter-api annotations: nginx.ingress.kubernetes.io/upstream-vhost: - "$service_name.$namespace.svc.cluster.local:$service_port" + "$service_name.$namespace.svc.cluster.local:$service_port" + nginx.ingress.kubernetes.io/auth-tls-verify-client: "optional" + nginx.ingress.kubernetes.io/auth-tls-secret: "natter-api/ca-secret" + nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1" + nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: + "true" spec: tls: - hosts: diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 2d46284..36e5840 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -1,6 +1,8 @@ package com.manning.apisecurityinaction.controller; -import java.nio.charset.StandardCharsets; +import java.io.*; +import java.net.URLDecoder; +import java.security.cert.*; import java.util.Base64; import com.lambdaworks.crypto.SCryptUtil; @@ -9,11 +11,13 @@ import org.json.JSONObject; import spark.*; +import static java.nio.charset.StandardCharsets.UTF_8; import static spark.Spark.halt; public class UserController { private static final String USERNAME_PATTERN = "[a-zA-Z][a-zA-Z0-9]{1,29}"; + private static final int DNS_TYPE = 2; private final Database database; @@ -46,6 +50,10 @@ public JSONObject registerUser(Request request, } public void authenticate(Request request, Response response) { + if ("SUCCESS".equals(request.headers("ssl-client-verify"))) { + processClientCertificateAuth(request); + return; + } var credentials = getCredentials(request); if (credentials == null) return; @@ -65,6 +73,31 @@ public void authenticate(Request request, Response response) { } } + void processClientCertificateAuth(Request request) { + var pem = request.headers("ssl-client-cert"); + pem = URLDecoder.decode(pem, UTF_8); + + try (var in = new ByteArrayInputStream(pem.getBytes(UTF_8))) { + var certFactory = CertificateFactory.getInstance("X.509"); + var cert = (X509Certificate) certFactory.generateCertificate(in); + + if (cert.getSubjectAlternativeNames() == null) { + return; + } + + for (var san : cert.getSubjectAlternativeNames()) { + if ((Integer) san.get(0) == DNS_TYPE) { + var subject = (String) san.get(1); + request.attribute("subject", subject); + return; + } + } + + } catch (CertificateException | IOException e) { + throw new RuntimeException(e); + } + } + String[] getCredentials(Request request) { var authHeader = request.headers("Authorization"); if (authHeader == null || !authHeader.startsWith("Basic ")) { @@ -73,7 +106,7 @@ String[] getCredentials(Request request) { var offset = "Basic ".length(); var credentials = new String(Base64.getDecoder().decode( - authHeader.substring(offset)), StandardCharsets.UTF_8); + authHeader.substring(offset)), UTF_8); var components = credentials.split(":", 2); if (components.length != 2) { From 5c7b87329c128c6a542486225c8ec00263ebb42c Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 27 Jan 2020 14:55:43 +0000 Subject: [PATCH 179/209] Allow password authentication to be disabled This solves the mini-project in section 11.4.3 --- .../controller/UserController.java | 16 ++++++++++------ natter-api/src/main/resources/schema.sql | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 36e5840..8a68b1d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -29,17 +29,21 @@ public JSONObject registerUser(Request request, Response response) throws Exception { var json = new JSONObject(request.body()); var username = json.getString("username"); - var password = json.getString("password"); + var password = json.optString("password", null); if (!username.matches(USERNAME_PATTERN)) { throw new IllegalArgumentException("invalid username"); } - if (password.length() < 8) { - throw new IllegalArgumentException( - "password must be at least 8 characters"); - } - var hash = SCryptUtil.scrypt(password, 32768, 8, 1); + String hash = null; + if (password != null) { + if (password.length() < 8) { + throw new IllegalArgumentException( + "password must be at least 8 characters"); + } + + hash = SCryptUtil.scrypt(password, 32768, 8, 1); + } database.updateUnique( "INSERT INTO users(user_id, pw_hash)" + " VALUES(?, ?)", username, hash); diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql index 24aa491..658154a 100644 --- a/natter-api/src/main/resources/schema.sql +++ b/natter-api/src/main/resources/schema.sql @@ -1,6 +1,6 @@ CREATE TABLE users( user_id VARCHAR(30) PRIMARY KEY, - pw_hash VARCHAR(255) NOT NULL + pw_hash VARCHAR(255) ); CREATE TABLE group_members( group_id VARCHAR(30), From 6b7ff660cf16b13440736556d0e81e4e9f9b2769 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 3 Feb 2020 16:20:35 +0000 Subject: [PATCH 180/209] Enforce mTLS certificate-bound access tokens --- .../com/manning/apisecurityinaction/Main.java | 8 ++++- .../controller/UserController.java | 20 ++++++----- .../token/OAuth2TokenStore.java | 34 +++++++++++++++++-- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 54a285d..1655992 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -1,6 +1,7 @@ package com.manning.apisecurityinaction; import java.io.FileInputStream; +import java.net.URI; import java.security.KeyStore; import java.sql.Connection; import java.util.Set; @@ -45,7 +46,12 @@ public static void main(String... args) throws Exception { SecureTokenStore tokenStore = HmacTokenStore.wrap( new DatabaseTokenStore(database), macKey); var capController = new CapabilityController(tokenStore); - var tokenController = new TokenController(tokenStore); + + var introspectionEndpoint = URI.create( + "http://as.example.com:8080/oauth2/instrospect"); + var oauthStore = new OAuth2TokenStore(introspectionEndpoint, + "rs", "password"); + var tokenController = new TokenController(oauthStore); var spaceController = new SpaceController(database, capController); var userController = new UserController(database); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index 8a68b1d..812cddd 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -79,16 +79,11 @@ public void authenticate(Request request, Response response) { void processClientCertificateAuth(Request request) { var pem = request.headers("ssl-client-cert"); - pem = URLDecoder.decode(pem, UTF_8); - - try (var in = new ByteArrayInputStream(pem.getBytes(UTF_8))) { - var certFactory = CertificateFactory.getInstance("X.509"); - var cert = (X509Certificate) certFactory.generateCertificate(in); - + var cert = decodeCert(pem); + try { if (cert.getSubjectAlternativeNames() == null) { return; } - for (var san : cert.getSubjectAlternativeNames()) { if ((Integer) san.get(0) == DNS_TYPE) { var subject = (String) san.get(1); @@ -96,8 +91,17 @@ void processClientCertificateAuth(Request request) { return; } } + } catch (CertificateParsingException e) { + throw new RuntimeException(e); + } + } - } catch (CertificateException | IOException e) { + public static X509Certificate decodeCert(String encodedCert) { + var pem = URLDecoder.decode(encodedCert, UTF_8); + try (var in = new ByteArrayInputStream(pem.getBytes(UTF_8))) { + var certFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certFactory.generateCertificate(in); + } catch (Exception e) { throw new RuntimeException(e); } } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java index 4f63a3b..53c943a 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java @@ -7,9 +7,11 @@ import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse.BodyHandlers; import java.security.*; +import java.security.cert.*; import java.time.Instant; import java.util.*; +import com.manning.apisecurityinaction.controller.UserController; import org.json.JSONObject; import spark.Request; @@ -99,7 +101,7 @@ public Optional read(Request request, String tokenId) { var json = new JSONObject(httpResponse.body()); if (json.getBoolean("active")) { - return processResponse(json); + return processResponse(json, request); } } } catch (IOException e) { @@ -112,10 +114,29 @@ public Optional read(Request request, String tokenId) { return Optional.empty(); } - private Optional processResponse(JSONObject response) { + private Optional processResponse(JSONObject response, + Request originalRequest) { var expiry = Instant.ofEpochSecond(response.getLong("exp")); var subject = response.getString("sub"); + var confirmationKey = response.optJSONObject("cnf"); + if (confirmationKey != null) { + for (var method : confirmationKey.keySet()) { + if (!"x5t#S256".equals(method)) { + throw new RuntimeException( + "Unknown confirmation method: " + method); + } + var expectedHash = Base64url.decode( + confirmationKey.getString(method)); + var cert = UserController.decodeCert( + originalRequest.headers("ssl-client-cert")); + var certHash = thumbprint(cert); + if (!MessageDigest.isEqual(expectedHash, certHash)) { + return Optional.empty(); + } + } + } + var token = new Token(expiry, subject); token.attributes.put("scope", response.getString("scope")); @@ -126,6 +147,15 @@ private Optional processResponse(JSONObject response) { } + private byte[] thumbprint(X509Certificate certificate) { + try { + var sha256 = MessageDigest.getInstance("SHA-256"); + return sha256.digest(certificate.getEncoded()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @Override public void revoke(Request request, String tokenId) { throw new UnsupportedOperationException(); From 199c13907aef5174103f03f179cf063b2f9b3f5b Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 5 Feb 2020 11:23:17 +0000 Subject: [PATCH 181/209] Read database password from Kubernetes secrets --- natter-api/kubernetes/natter-api-deployment.yaml | 10 +++++++++- .../java/com/manning/apisecurityinaction/Main.java | 7 ++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/natter-api/kubernetes/natter-api-deployment.yaml b/natter-api/kubernetes/natter-api-deployment.yaml index be3ca52..0df017d 100644 --- a/natter-api/kubernetes/natter-api-deployment.yaml +++ b/natter-api/kubernetes/natter-api-deployment.yaml @@ -19,6 +19,10 @@ spec: - name: natter-api image: apisecurityinaction/natter-api:latest imagePullPolicy: Never + volumeMounts: + - name: db-password + mountPath: "/etc/secrets/database" + readOnly: true securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true @@ -26,4 +30,8 @@ spec: drop: - all ports: - - containerPort: 4567 \ No newline at end of file + - containerPort: 4567 + volumes: + - name: db-password + secret: + secretName: db-password \ No newline at end of file diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 1655992..497a582 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -2,6 +2,7 @@ import java.io.FileInputStream; import java.net.URI; +import java.nio.file.*; import java.security.KeyStore; import java.sql.Connection; import java.util.Set; @@ -30,9 +31,13 @@ public static void main(String... args) throws Exception { port(args.length > 0 ? Integer.parseInt(args[0]) : SPARK_DEFAULT_PORT); + var secretsPath = Paths.get("/etc/secrets/database"); + var dbUsername = Files.readString(secretsPath.resolve("username")); + var dbPassword = Files.readString(secretsPath.resolve("password")); + var jdbcUrl = "jdbc:h2:tcp://natter-database-service:9092/mem:natter"; var datasource = JdbcConnectionPool.create( - jdbcUrl, "natter", "password"); + jdbcUrl, dbUsername, dbPassword); createTables(datasource.getConnection()); datasource = JdbcConnectionPool.create( jdbcUrl, "natter_api_user", "password"); From 387638bca5ccb28293c11a17fbba3105bc21c7fb Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 7 Feb 2020 10:45:27 +0000 Subject: [PATCH 182/209] Use HKDF to derive keys without storing them --- .../com/manning/apisecurityinaction/HKDF.java | 32 +++++++++++++++++++ .../com/manning/apisecurityinaction/Main.java | 11 +++++++ 2 files changed, 43 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java b/natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java new file mode 100644 index 0000000..7823476 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java @@ -0,0 +1,32 @@ +package com.manning.apisecurityinaction; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.*; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.checkIndex; + +public class HKDF { + public static Key expand(Key masterKey, String context, + int outputKeySize, String algorithm) + throws GeneralSecurityException { + checkIndex(outputKeySize, 255*32); + + var hmac = Mac.getInstance("HmacSHA256"); + hmac.init(masterKey); + + var output = new byte[outputKeySize]; + var block = new byte[0]; + for (int i = 0; i < outputKeySize; i += 32) { + hmac.update(block); + hmac.update(context.getBytes(UTF_8)); + hmac.update((byte) ((i / 32) + 1)); + block = hmac.doFinal(); + System.arraycopy(block, 0, output, i, + Math.min(outputKeySize - i, 32)); + } + + return new SecretKeySpec(output, algorithm); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java index 497a582..4dfba93 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java @@ -14,6 +14,7 @@ import org.dalesbred.result.EmptyResultException; import org.h2.jdbcx.JdbcConnectionPool; import org.json.*; +import software.pando.crypto.nacl.Crypto; import spark.*; import spark.embeddedserver.EmbeddedServers; import spark.embeddedserver.jetty.EmbeddedJettyFactory; @@ -48,6 +49,16 @@ public static void main(String... args) throws Exception { "changeit".toCharArray()); var macKey = keystore.getKey("hmac-key", "changeit".toCharArray()); + // Examples of deriving keys using HKDF: + // Derive a symmetric AES key + var encKey = HKDF.expand(macKey, "token-encryption-key", + 32, "AES"); + + // Derive an Ed25519 signature key pair + var seed = HKDF.expand(macKey, "nacl-signing-key-seed", + 32, "NaCl"); + var keyPair = Crypto.seedSigningKeyPair(seed.getEncoded()); + SecureTokenStore tokenStore = HmacTokenStore.wrap( new DatabaseTokenStore(database), macKey); var capController = new CapabilityController(tokenStore); From f17999c0d950fd4bd7025e15bc57481d7abc7156 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 7 Feb 2020 10:59:12 +0000 Subject: [PATCH 183/209] Update README.md for chapter 11 --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index ed2e94d..ea6f79e 100644 --- a/README.md +++ b/README.md @@ -84,3 +84,8 @@ descriptions for HTTP requests that can be - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter10) - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter10-end) + +### Chapter 11 - Securing service to service APIs + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter11) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter11-end) From 06961b0e1f6795ce9192537b6fe3177bac074f46 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 10 Feb 2020 14:36:31 +0000 Subject: [PATCH 184/209] Ensure client cert auth succeeded --- .../manning/apisecurityinaction/token/OAuth2TokenStore.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java index 53c943a..7c92af2 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java @@ -126,6 +126,10 @@ private Optional processResponse(JSONObject response, throw new RuntimeException( "Unknown confirmation method: " + method); } + if (!"SUCCESS".equals( + originalRequest.headers("ssl-client-verify"))) { + return Optional.empty(); + } var expectedHash = Base64url.decode( confirmationKey.getString(method)); var cert = UserController.decodeCert( From 8550ce44b46cf21cc0fed379ef14184a43fa5632 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 17 Feb 2020 14:44:28 +0000 Subject: [PATCH 185/209] Rationalize use of the term "service" to avoid confusion --- ...oyment.yaml => natter-link-preview-deployment.yaml} | 10 +++++----- natter-api/kubernetes/natter-link-preview-service.yaml | 2 +- .../{LinkPreviewService.java => LinkPreviewer.java} | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename natter-api/kubernetes/{natter-link-preview-service-deployment.yaml => natter-link-preview-deployment.yaml} (70%) rename natter-api/src/main/java/com/manning/apisecurityinaction/{LinkPreviewService.java => LinkPreviewer.java} (97%) diff --git a/natter-api/kubernetes/natter-link-preview-service-deployment.yaml b/natter-api/kubernetes/natter-link-preview-deployment.yaml similarity index 70% rename from natter-api/kubernetes/natter-link-preview-service-deployment.yaml rename to natter-api/kubernetes/natter-link-preview-deployment.yaml index b1e2816..4100f4a 100644 --- a/natter-api/kubernetes/natter-link-preview-service-deployment.yaml +++ b/natter-api/kubernetes/natter-link-preview-deployment.yaml @@ -1,23 +1,23 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: link-preview-service-deployment + name: link-preview-deployment namespace: natter-api spec: selector: matchLabels: - app: link-preview-service + app: link-preview replicas: 1 template: metadata: labels: - app: link-preview-service + app: link-preview spec: securityContext: runAsNonRoot: true containers: - - name: link-preview-service - image: apisecurityinaction/link-preview-service:latest + - name: link-preview + image: apisecurityinaction/link-preview:latest imagePullPolicy: Never securityContext: allowPrivilegeEscalation: false diff --git a/natter-api/kubernetes/natter-link-preview-service.yaml b/natter-api/kubernetes/natter-link-preview-service.yaml index bd0f401..d06e18b 100644 --- a/natter-api/kubernetes/natter-link-preview-service.yaml +++ b/natter-api/kubernetes/natter-link-preview-service.yaml @@ -5,7 +5,7 @@ metadata: namespace: natter-api spec: selector: - app: link-preview-service + app: link-preview ports: - protocol: TCP port: 4567 \ No newline at end of file diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java b/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewer.java similarity index 97% rename from natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java rename to natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewer.java index 93d3683..5a997d8 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewService.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewer.java @@ -13,9 +13,9 @@ import static org.jsoup.Connection.Method.GET; import static spark.Spark.*; -public class LinkPreviewService { +public class LinkPreviewer { private static final Logger logger = - LoggerFactory.getLogger(LinkPreviewService.class); + LoggerFactory.getLogger(LinkPreviewer.class); public static void main(String...args) { afterAfter((request, response) -> { From 5b9ef2147b6893f9feaa0c4f3145fdec9ab48f7c Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 25 Feb 2020 13:06:50 +0000 Subject: [PATCH 186/209] Fix scope of constant --- .../manning/apisecurityinaction/controller/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java index dad72d8..3c2e1e4 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java @@ -15,7 +15,7 @@ import static spark.Spark.halt; public class UserController { - private static final String USERNAME_PATTERN = + static final String USERNAME_PATTERN = "[a-zA-Z][a-zA-Z0-9]{1,29}"; private static final int DNS_TYPE = 2; From ca806c19c8b763dc93a46c0c0ed1e8cd17d49452 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 25 Feb 2020 15:39:40 +0000 Subject: [PATCH 187/209] A simple UDP client and server --- .../apisecurityinaction/UdpClient.java | 31 +++++++++++++++++++ .../apisecurityinaction/UdpServer.java | 27 ++++++++++++++++ natter-api/test.txt | 26 ++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java create mode 100644 natter-api/test.txt diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java new file mode 100644 index 0000000..c9b4ef9 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java @@ -0,0 +1,31 @@ +package com.manning.apisecurityinaction; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class UdpClient { + public static void main(String... args) throws Exception { + var serverAddr = new InetSocketAddress("localhost", 54321); + + try (var socket = new DatagramSocket(); + var in = Files.newBufferedReader(Paths.get("test.txt"))) { + var buffer = new byte[UdpServer.PACKET_SIZE]; + var packet = new DatagramPacket(buffer, buffer.length); + packet.setSocketAddress(serverAddr); + + String line; + while ((line = in.readLine()) != null) { + System.out.println("Sending packet to server"); + packet.setData(line.getBytes(UTF_8)); + socket.send(packet); + } + + System.out.println("All packets sent"); + } + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java new file mode 100644 index 0000000..8567a46 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java @@ -0,0 +1,27 @@ +package com.manning.apisecurityinaction; + +import java.net.DatagramPacket; +import java.net.DatagramSocket; + +public class UdpServer { + static final int PACKET_SIZE = 1024; + + public static void main(String... args) throws Exception { + try (var socket = new DatagramSocket(54321)) { + System.out.printf("Listening on port %d%n", + socket.getLocalPort()); + + var buffer = new byte[PACKET_SIZE]; + var packet = new DatagramPacket(buffer, PACKET_SIZE); + + while (true) { + socket.receive(packet); + System.out.printf("Received packet from %s - len: %d%n", + packet.getSocketAddress(), packet.getLength()); + + var message = new String(buffer, 0, packet.getLength()); + System.out.println("Message: " + message); + } + } + } +} diff --git a/natter-api/test.txt b/natter-api/test.txt new file mode 100644 index 0000000..38e0270 --- /dev/null +++ b/natter-api/test.txt @@ -0,0 +1,26 @@ +Lorem ipsum dolor sit amet consectetur adipiscing elit ultrices platea, dapibus eleifend dictum facilisis aliquam +egestas pretium vehicula, euismod metus mauris sociis justo aptent congue fames. Auctor tellus nunc at cursus morbi +viverra velit nisl purus, a malesuada scelerisque sem lectus augue etiam ornare magnis donec, non elementum facilisis in +penatibus aliquet nulla per. Placerat posuere mauris dictumst dis volutpat eleifend natoque nascetur, iaculis curabitur +velit nam sociis erat tristique lobortis, sapien convallis ridiculus magna lacinia cras torquent. Proin pretium commodo +torquent scelerisque conubia etiam eu, nec integer erat ante turpis id et, lobortis rhoncus varius fringilla ad +parturient. Purus id in a tristique nostra vitae fames, bibendum consequat porta montes magna vestibulum ullamcorper +neque, nascetur tincidunt habitant fusce hac risus. In mi aptent eros euismod odio dictumst diam, orci vulputate +parturient cursus hendrerit lobortis ante sodales, litora vivamus cum felis convallis pharetra. Etiam dis duis vel +vulputate scelerisque lacus sem platea praesent primis fermentum vehicula, egestas nec convallis facilisis dictumst +ridiculus inceptos et venenatis penatibus. Hendrerit magnis senectus metus aliquet volutpat curae dapibus mollis posuere +leo, sem blandit nullam tincidunt condimentum sociis tortor enim porta imperdiet, lectus mattis class vehicula facilisi +bibendum vivamus penatibus vestibulum. Porttitor justo suscipit pharetra nam potenti lobortis tempus, hac molestie morbi +sociosqu per vel penatibus, curabitur primis viverra tincidunt nisi nec. Lacinia pellentesque arcu per imperdiet duis +bibendum aliquam tortor risus varius placerat massa, ultricies cubilia nullam cras quam tristique laoreet et metus +integer. Phasellus suspendisse taciti duis blandit montes nam magna class per, ante senectus praesent pulvinar vel ut +fringilla ad, aenean nulla tristique mattis molestie nunc habitasse vitae. Purus porta ornare scelerisque aptent quam +ullamcorper nec lectus at gravida dis, sem rhoncus eleifend tempor est parturient iaculis curabitur vivamus dictumst. +Dignissim nullam condimentum tristique leo lobortis ornare tortor congue class non, mattis velit interdum primis +ultrices fringilla donec vivamus facilisis purus habitant, sed fames aliquet cum netus justo cras placerat dis. Enim ad +fringilla est sed vehicula in nam dignissim, malesuada aenean curabitur ut montes lobortis augue dictumst cursus, metus +neque mus gravida maecenas tristique ligula. Aptent quis blandit tincidunt posuere nisi sem, malesuada nunc viverra +bibendum montes venenatis cras, suscipit feugiat pulvinar nibh mi. Placerat facilisi in class eros diam erat metus +egestas, volutpat natoque aenean ad sapien facilisis ac eget pretium, velit tincidunt ligula porttitor tempus lacus +conubia. Quam suspendisse condimentum pretium lectus facilisis curae accumsan tincidunt, duis torquent cursus blandit +pellentesque fringilla luctus cum mauris, nam aenean sed interdum di. From 816e8ca797fe234790cde2d48c376d2b2112aaf7 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 26 Feb 2020 16:32:33 +0000 Subject: [PATCH 188/209] Initial DTLS implementation (WiP) --- .../DTLSDatagramSocket.java | 172 ++++++++++++++++++ .../apisecurityinaction/UdpClient.java | 28 ++- .../apisecurityinaction/UdpServer.java | 25 ++- 3 files changed, 215 insertions(+), 10 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DTLSDatagramSocket.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DTLSDatagramSocket.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DTLSDatagramSocket.java new file mode 100644 index 0000000..865178c --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DTLSDatagramSocket.java @@ -0,0 +1,172 @@ +package com.manning.apisecurityinaction; + +import static com.manning.apisecurityinaction.UdpServer.PACKET_SIZE; + +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; + +import javax.net.ssl.*; +import javax.net.ssl.SSLEngineResult.*; + +import org.apache.commons.codec.binary.Hex; +import org.slf4j.*; + +public class DTLSDatagramSocket extends DatagramSocket { + private static final Logger logger = LoggerFactory.getLogger(DTLSDatagramSocket.class); + + private final SSLContext sslContext; + + private SSLEngine sslEngine; + + public DTLSDatagramSocket(SSLContext sslContext) throws SocketException { + this(sslContext, 0); + } + + public DTLSDatagramSocket(SSLContext sslContext, int port) throws SocketException { + super(port); + if (!"DTLS".equalsIgnoreCase(sslContext.getProtocol())) { + throw new IllegalArgumentException("SSLContext not for DTLS"); + } + this.sslContext = sslContext; + } + + @Override + public void send(DatagramPacket packet) throws IOException { + + if (sslEngine == null || !sslEngine.getUseClientMode() || + !sslEngine.getPeerHost().equals(packet.getAddress().getHostName()) || + sslEngine.getPeerPort() != packet.getPort()) { + + sslEngine = sslContext.createSSLEngine( + packet.getAddress().getHostName(), + packet.getPort()); + var params = sslEngine.getSSLParameters(); + params.setMaximumPacketSize(packet.getData().length); + sslEngine.setSSLParameters(params); + sslEngine.setUseClientMode(true); + + handshake(sslEngine); + } + + if (sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING) { + throw new IllegalStateException("DTLS handshake failed"); + } + + var sendBuffer = ByteBuffer.wrap(packet.getData(), + packet.getOffset(), packet.getLength()); + var networkBuffer = ByteBuffer.allocate(16384); + var result = sslEngine.wrap(sendBuffer, networkBuffer); + if (result.getStatus() != Status.OK) { + throw new IOException("Error creating DTLS packet: " + + result); + } + + networkBuffer.flip(); + var buffer = new byte[networkBuffer.remaining()]; + networkBuffer.get(buffer); + packet = new DatagramPacket(buffer, buffer.length, + packet.getSocketAddress()); + logger.info("Sending packet: " + Hex.encodeHexString(buffer)); + super.send(packet); + } + + @Override + public synchronized void receive(DatagramPacket packet) throws IOException { + if (sslEngine == null || sslEngine.getUseClientMode()) { + + sslEngine = sslContext.createSSLEngine(); + var params = sslEngine.getSSLParameters(); + params.setMaximumPacketSize(packet.getData().length); + sslEngine.setSSLParameters(params); + sslEngine.setUseClientMode(false); + + handshake(sslEngine); + } + super.receive(packet); + logger.info("Received packet length={}", packet.getLength()); + logger.info("Packet size: {}", packet.getData().length); + + var network = ByteBuffer.wrap(packet.getData(), + packet.getOffset(), packet.getLength()); + var application = ByteBuffer.allocate(packet.getData().length); + var result = sslEngine.unwrap(network, application); + if (result.getStatus() != Status.OK) { + throw new IOException("DTLS error: " + result); + } + + var len = application.flip().remaining(); + application.get(packet.getData(), 0, + Math.min(len, packet.getData().length)); + packet.setLength(len); + } + + private void handshake(SSLEngine engine) throws IOException { + logger.info("Beginning DTLS handshake"); + engine.beginHandshake(); + + var packet = new DatagramPacket(new byte[PACKET_SIZE], PACKET_SIZE); + packet.setSocketAddress(new InetSocketAddress("localhost", 54321)); + + ByteBuffer networkData; + ByteBuffer applicationData = ByteBuffer.allocate(PACKET_SIZE); + var status = engine.getHandshakeStatus(); + SSLEngineResult result; + while (status != HandshakeStatus.FINISHED) { + logger.info("Status: " + status); + switch (status) { + case NEED_UNWRAP: + packet.setData(new byte[PACKET_SIZE]); + super.receive(packet); + logger.info("Packed received, size={}", packet.getLength()); + networkData = ByteBuffer.wrap(packet.getData(), 0, + packet.getLength()); + applicationData = ByteBuffer.allocate(PACKET_SIZE); + result = engine.unwrap(networkData, applicationData); + logger.info("Unwrap result: {}", result); + + status = result.getHandshakeStatus(); + break; + case NEED_UNWRAP_AGAIN: + networkData = ByteBuffer.allocate(0); + applicationData = ByteBuffer.allocate(PACKET_SIZE); + result = engine.unwrap(networkData, applicationData); + logger.info("Unwrap result: {}", result); + status = result.getHandshakeStatus(); + break; + case NEED_TASK: + Runnable task; + while ((task = engine.getDelegatedTask()) != null) { + logger.info("Running task: {}", task); + task.run(); + } + status = engine.getHandshakeStatus(); + logger.info("Tasks executed"); + break; + case NEED_WRAP: + applicationData = ByteBuffer.allocate(0); + networkData = ByteBuffer.allocate(32768); + result = engine.wrap(applicationData, networkData); + logger.info("Wrap result: " + result); + status = result.getHandshakeStatus(); + networkData.flip(); + + if (networkData.hasRemaining()) { + var buffer = new byte[networkData.remaining()]; + networkData.get(buffer); + packet.setData(buffer); + logger.info("Sending packet to: {}, size={}", + packet.getSocketAddress(), + packet.getLength()); + super.send(packet); + } + break; + default: + throw new IllegalStateException( + "Unexpected handshake state: " + status); + } + } + logger.info("Handshake finished!"); + } + +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java index c9b4ef9..8aa8ae4 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java @@ -2,17 +2,18 @@ import static java.nio.charset.StandardCharsets.UTF_8; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetSocketAddress; -import java.nio.file.Files; -import java.nio.file.Paths; +import java.io.FileInputStream; +import java.net.*; +import java.nio.file.*; +import java.security.KeyStore; + +import javax.net.ssl.*; public class UdpClient { public static void main(String... args) throws Exception { var serverAddr = new InetSocketAddress("localhost", 54321); - try (var socket = new DatagramSocket(); + try (var socket = new DTLSDatagramSocket(getSslContext()); var in = Files.newBufferedReader(Paths.get("test.txt"))) { var buffer = new byte[UdpServer.PACKET_SIZE]; var packet = new DatagramPacket(buffer, buffer.length); @@ -28,4 +29,19 @@ public static void main(String... args) throws Exception { System.out.println("All packets sent"); } } + + private static SSLContext getSslContext() throws Exception { + var sslContext = SSLContext.getInstance("DTLS"); + + var trustStore = KeyStore.getInstance("PKCS12"); + trustStore.load(new FileInputStream("localhost.p12"), + "changeit".toCharArray()); + + var trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + return sslContext; + } } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java index 8567a46..3f399b6 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java @@ -1,27 +1,44 @@ package com.manning.apisecurityinaction; +import java.io.FileInputStream; import java.net.DatagramPacket; -import java.net.DatagramSocket; +import java.security.KeyStore; + +import javax.net.ssl.*; public class UdpServer { static final int PACKET_SIZE = 1024; public static void main(String... args) throws Exception { - try (var socket = new DatagramSocket(54321)) { + try (var socket = new DTLSDatagramSocket(getSslContext(), 54321)) { System.out.printf("Listening on port %d%n", socket.getLocalPort()); var buffer = new byte[PACKET_SIZE]; - var packet = new DatagramPacket(buffer, PACKET_SIZE); while (true) { + var packet = new DatagramPacket(buffer, PACKET_SIZE); socket.receive(packet); System.out.printf("Received packet from %s - len: %d%n", packet.getSocketAddress(), packet.getLength()); - var message = new String(buffer, 0, packet.getLength()); System.out.println("Message: " + message); } } } + + private static SSLContext getSslContext() throws Exception { + var sslContext = SSLContext.getInstance("DTLS"); + + var keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new FileInputStream("localhost.p12"), + "changeit".toCharArray()); + + var keyManager = KeyManagerFactory.getInstance( + KeyManagerFactory.getDefaultAlgorithm()); + keyManager.init(keyStore, "changeit".toCharArray()); + + sslContext.init(keyManager.getKeyManagers(), null, null); + return sslContext; + } } From 8e0c172d051ba1dbd622d5f496224b7266b9b216 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 4 Mar 2020 20:22:57 +0000 Subject: [PATCH 189/209] Rename classes --- .../DTLSDatagramSocket.java | 172 ------------- .../{UdpClient.java => DtlsClient.java} | 36 +-- .../DtlsDatagramChannel.java | 189 +++++++++++++++ .../DtlsDatagramSocket.java | 227 ++++++++++++++++++ .../apisecurityinaction/DtlsPacketDebug.java | 126 ++++++++++ .../apisecurityinaction/DtlsServer.java | 42 ++++ .../apisecurityinaction/UdpServer.java | 44 ---- 7 files changed, 604 insertions(+), 232 deletions(-) delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DTLSDatagramSocket.java rename natter-api/src/main/java/com/manning/apisecurityinaction/{UdpClient.java => DtlsClient.java} (56%) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramSocket.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DtlsPacketDebug.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DTLSDatagramSocket.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DTLSDatagramSocket.java deleted file mode 100644 index 865178c..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DTLSDatagramSocket.java +++ /dev/null @@ -1,172 +0,0 @@ -package com.manning.apisecurityinaction; - -import static com.manning.apisecurityinaction.UdpServer.PACKET_SIZE; - -import java.io.IOException; -import java.net.*; -import java.nio.ByteBuffer; - -import javax.net.ssl.*; -import javax.net.ssl.SSLEngineResult.*; - -import org.apache.commons.codec.binary.Hex; -import org.slf4j.*; - -public class DTLSDatagramSocket extends DatagramSocket { - private static final Logger logger = LoggerFactory.getLogger(DTLSDatagramSocket.class); - - private final SSLContext sslContext; - - private SSLEngine sslEngine; - - public DTLSDatagramSocket(SSLContext sslContext) throws SocketException { - this(sslContext, 0); - } - - public DTLSDatagramSocket(SSLContext sslContext, int port) throws SocketException { - super(port); - if (!"DTLS".equalsIgnoreCase(sslContext.getProtocol())) { - throw new IllegalArgumentException("SSLContext not for DTLS"); - } - this.sslContext = sslContext; - } - - @Override - public void send(DatagramPacket packet) throws IOException { - - if (sslEngine == null || !sslEngine.getUseClientMode() || - !sslEngine.getPeerHost().equals(packet.getAddress().getHostName()) || - sslEngine.getPeerPort() != packet.getPort()) { - - sslEngine = sslContext.createSSLEngine( - packet.getAddress().getHostName(), - packet.getPort()); - var params = sslEngine.getSSLParameters(); - params.setMaximumPacketSize(packet.getData().length); - sslEngine.setSSLParameters(params); - sslEngine.setUseClientMode(true); - - handshake(sslEngine); - } - - if (sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING) { - throw new IllegalStateException("DTLS handshake failed"); - } - - var sendBuffer = ByteBuffer.wrap(packet.getData(), - packet.getOffset(), packet.getLength()); - var networkBuffer = ByteBuffer.allocate(16384); - var result = sslEngine.wrap(sendBuffer, networkBuffer); - if (result.getStatus() != Status.OK) { - throw new IOException("Error creating DTLS packet: " + - result); - } - - networkBuffer.flip(); - var buffer = new byte[networkBuffer.remaining()]; - networkBuffer.get(buffer); - packet = new DatagramPacket(buffer, buffer.length, - packet.getSocketAddress()); - logger.info("Sending packet: " + Hex.encodeHexString(buffer)); - super.send(packet); - } - - @Override - public synchronized void receive(DatagramPacket packet) throws IOException { - if (sslEngine == null || sslEngine.getUseClientMode()) { - - sslEngine = sslContext.createSSLEngine(); - var params = sslEngine.getSSLParameters(); - params.setMaximumPacketSize(packet.getData().length); - sslEngine.setSSLParameters(params); - sslEngine.setUseClientMode(false); - - handshake(sslEngine); - } - super.receive(packet); - logger.info("Received packet length={}", packet.getLength()); - logger.info("Packet size: {}", packet.getData().length); - - var network = ByteBuffer.wrap(packet.getData(), - packet.getOffset(), packet.getLength()); - var application = ByteBuffer.allocate(packet.getData().length); - var result = sslEngine.unwrap(network, application); - if (result.getStatus() != Status.OK) { - throw new IOException("DTLS error: " + result); - } - - var len = application.flip().remaining(); - application.get(packet.getData(), 0, - Math.min(len, packet.getData().length)); - packet.setLength(len); - } - - private void handshake(SSLEngine engine) throws IOException { - logger.info("Beginning DTLS handshake"); - engine.beginHandshake(); - - var packet = new DatagramPacket(new byte[PACKET_SIZE], PACKET_SIZE); - packet.setSocketAddress(new InetSocketAddress("localhost", 54321)); - - ByteBuffer networkData; - ByteBuffer applicationData = ByteBuffer.allocate(PACKET_SIZE); - var status = engine.getHandshakeStatus(); - SSLEngineResult result; - while (status != HandshakeStatus.FINISHED) { - logger.info("Status: " + status); - switch (status) { - case NEED_UNWRAP: - packet.setData(new byte[PACKET_SIZE]); - super.receive(packet); - logger.info("Packed received, size={}", packet.getLength()); - networkData = ByteBuffer.wrap(packet.getData(), 0, - packet.getLength()); - applicationData = ByteBuffer.allocate(PACKET_SIZE); - result = engine.unwrap(networkData, applicationData); - logger.info("Unwrap result: {}", result); - - status = result.getHandshakeStatus(); - break; - case NEED_UNWRAP_AGAIN: - networkData = ByteBuffer.allocate(0); - applicationData = ByteBuffer.allocate(PACKET_SIZE); - result = engine.unwrap(networkData, applicationData); - logger.info("Unwrap result: {}", result); - status = result.getHandshakeStatus(); - break; - case NEED_TASK: - Runnable task; - while ((task = engine.getDelegatedTask()) != null) { - logger.info("Running task: {}", task); - task.run(); - } - status = engine.getHandshakeStatus(); - logger.info("Tasks executed"); - break; - case NEED_WRAP: - applicationData = ByteBuffer.allocate(0); - networkData = ByteBuffer.allocate(32768); - result = engine.wrap(applicationData, networkData); - logger.info("Wrap result: " + result); - status = result.getHandshakeStatus(); - networkData.flip(); - - if (networkData.hasRemaining()) { - var buffer = new byte[networkData.remaining()]; - networkData.get(buffer); - packet.setData(buffer); - logger.info("Sending packet to: {}, size={}", - packet.getSocketAddress(), - packet.getLength()); - super.send(packet); - } - break; - default: - throw new IllegalStateException( - "Unexpected handshake state: " + status); - } - } - logger.info("Handshake finished!"); - } - -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java similarity index 56% rename from natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java rename to natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java index 8aa8ae4..f1d7d19 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/UdpClient.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java @@ -1,47 +1,51 @@ package com.manning.apisecurityinaction; -import static java.nio.charset.StandardCharsets.UTF_8; - +import javax.net.ssl.*; import java.io.FileInputStream; -import java.net.*; import java.nio.file.*; import java.security.KeyStore; -import javax.net.ssl.*; +import static java.nio.charset.StandardCharsets.UTF_8; -public class UdpClient { +public class DtlsClient { public static void main(String... args) throws Exception { - var serverAddr = new InetSocketAddress("localhost", 54321); - try (var socket = new DTLSDatagramSocket(getSslContext()); + try (var channel = new DtlsDatagramChannel(getClientContext(), sslParameters()); var in = Files.newBufferedReader(Paths.get("test.txt"))) { - var buffer = new byte[UdpServer.PACKET_SIZE]; - var packet = new DatagramPacket(buffer, buffer.length); - packet.setSocketAddress(serverAddr); + channel.connect("localhost", 54321); String line; while ((line = in.readLine()) != null) { System.out.println("Sending packet to server"); - packet.setData(line.getBytes(UTF_8)); - socket.send(packet); + channel.send(line.getBytes(UTF_8)); } System.out.println("All packets sent"); } } - private static SSLContext getSslContext() throws Exception { + private static SSLContext getClientContext() throws Exception { var sslContext = SSLContext.getInstance("DTLS"); var trustStore = KeyStore.getInstance("PKCS12"); - trustStore.load(new FileInputStream("localhost.p12"), + trustStore.load(new FileInputStream("as.example.com.ca.p12"), "changeit".toCharArray()); var trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); + "PKIX"); trustManagerFactory.init(trustStore); - sslContext.init(null, trustManagerFactory.getTrustManagers(), null); + sslContext.init(null, trustManagerFactory.getTrustManagers(), + null); return sslContext; } + + static SSLParameters sslParameters() { + var params = new SSLParameters(); + params.setProtocols(new String[] { "DTLSv1.2" }); + params.setMaximumPacketSize(1500); + params.setEnableRetransmissions(true); + params.setEndpointIdentificationAlgorithm("HTTPS"); + return params; + } } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java new file mode 100644 index 0000000..0de747f --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java @@ -0,0 +1,189 @@ +package com.manning.apisecurityinaction; + +import javax.net.ssl.*; +import javax.net.ssl.SSLEngineResult.*; +import java.io.*; +import java.net.*; +import java.nio.*; +import java.nio.channels.*; + +import org.slf4j.*; + +public class DtlsDatagramChannel implements Closeable { + private static final Logger logger = LoggerFactory.getLogger(DtlsDatagramChannel.class); + + private final DatagramChannel channel; + private final SSLContext sslContext; + private final SSLParameters sslParameters; + + private final ByteBuffer netRecvBuffer; + private final ByteBuffer netSendBuffer; + private final ByteBuffer appBuffer; + + private SSLEngine sslEngine; + + public DtlsDatagramChannel(SSLContext sslContext, SSLParameters sslParameters) throws IOException { + this.channel = DatagramChannel.open(); + this.sslContext = sslContext; + this.sslParameters = sslParameters; + + this.netRecvBuffer = ByteBuffer.allocate(2048); + this.netSendBuffer = ByteBuffer.allocate(65535); + this.appBuffer = ByteBuffer.allocate(2048); + } + + public void bind(int port) throws IOException { + channel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); + } + + public void connect(String host, int port) throws IOException { + channel.connect(new InetSocketAddress(host, port)); + } + + public void send(byte[] data) throws IOException { + if (!channel.isConnected()) { + throw new IllegalStateException("Channel must be connected"); + } + + if (sslEngine == null) { + var socketAddr = ((InetSocketAddress) channel.getRemoteAddress()); + sslEngine = sslContext.createSSLEngine(socketAddr.getHostName(), socketAddr.getPort()); + sslEngine.setUseClientMode(true); + sslEngine.setSSLParameters(sslParameters); + + handshake(sslEngine); + } + + if (sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING) { + throw new IllegalStateException("DTLS handshake failed"); + } + + appBuffer.put(data); + appBuffer.flip(); + var result = sslEngine.wrap(appBuffer, netSendBuffer); + appBuffer.compact(); + + if (result.getStatus() != Status.OK) { + throw new IllegalStateException("Wrap failed: " + result); + } + + netSendBuffer.flip(); + channel.write(netSendBuffer); + netSendBuffer.compact(); + } + + public InetSocketAddress receive(ByteBuffer buffer) throws IOException { + var address = (InetSocketAddress) channel.receive(netRecvBuffer); + if (!channel.isConnected()) { + channel.connect(address); + } + if (sslEngine == null) { + sslEngine = sslContext.createSSLEngine(address.getHostName(), address.getPort()); + sslEngine.setUseClientMode(false); + sslEngine.setSSLParameters(sslParameters); + + handshake(sslEngine); + } + + channel.receive(netRecvBuffer); + netRecvBuffer.flip(); + var result = sslEngine.unwrap(netRecvBuffer, buffer); + netRecvBuffer.compact(); + + if (result.getStatus() == Status.BUFFER_UNDERFLOW) { + throw new BufferUnderflowException(); + } + if (result.getStatus() == Status.BUFFER_OVERFLOW) { + throw new BufferOverflowException(); + } + if (result.getStatus() == Status.CLOSED) { + throw new ClosedChannelException(); + } + + return address; + } + + @Override + public void close() throws IOException { + sslEngine.closeOutbound(); + sslEngine.closeInbound(); + } + + private void handshake(SSLEngine engine) throws IOException { + if (!channel.isConnected()) { + throw new IllegalStateException("Channel must be connected"); + } + logger.debug("Beginning DTLS handshake"); + engine.beginHandshake(); + + var status = engine.getHandshakeStatus(); + SSLEngineResult result; + while (status != HandshakeStatus.FINISHED) { + logger.debug("Handshake status: " + status); + switch (status) { + case NEED_UNWRAP: + netRecvBuffer.flip(); + result = engine.unwrap(netRecvBuffer, appBuffer); + netRecvBuffer.compact(); + logger.debug("Unwrap result: {}", result); + + if (result.getStatus() == Status.BUFFER_UNDERFLOW) { + channel.receive(netRecvBuffer); + netRecvBuffer.flip(); + DtlsPacketDebug.debug(netRecvBuffer); + result = engine.unwrap(netRecvBuffer, appBuffer); + netRecvBuffer.compact(); + } + + if (result.getStatus() != Status.OK) { + throw new IllegalStateException("Unwrap failed: " + result); + } + + status = result.getHandshakeStatus(); + break; + case NEED_UNWRAP_AGAIN: + netRecvBuffer.flip(); + result = engine.unwrap(netRecvBuffer, appBuffer); + netRecvBuffer.compact(); + logger.debug("Unwrap result: {}", result); + + if (result.getStatus() != Status.OK) { + throw new IllegalStateException("Unwrap failed: " + result); + } + + status = result.getHandshakeStatus(); + break; + case NEED_TASK: + Runnable task; + while ((task = engine.getDelegatedTask()) != null) { + logger.debug("Running task: {}", task); + task.run(); + } + status = engine.getHandshakeStatus(); + logger.debug("Tasks executed"); + break; + case NEED_WRAP: + result = engine.wrap(appBuffer, netSendBuffer); + appBuffer.compact(); + logger.debug("Wrap result: " + result); + + if (result.getStatus() != Status.OK) { + throw new IllegalStateException("Wrap failed: " + result); + } + + netSendBuffer.flip(); + DtlsPacketDebug.debug(netSendBuffer); + if (netSendBuffer.hasRemaining()) { + channel.write(netSendBuffer); + netSendBuffer.compact(); + } + status = result.getHandshakeStatus(); + break; + default: + throw new IllegalStateException( + "Unexpected handshake state: " + status); + } + } + logger.debug("Handshake finished!"); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramSocket.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramSocket.java new file mode 100644 index 0000000..6e616b4 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramSocket.java @@ -0,0 +1,227 @@ +package com.manning.apisecurityinaction; + +import javax.net.ssl.*; +import javax.net.ssl.SSLEngineResult.*; +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; + +import org.slf4j.*; + +import static java.util.Objects.requireNonNull; + +/** + * A wrapper over the UDP {@link DatagramSocket} that provides support + * for DTLS transport security. A DTLS handshake will be performed + */ +public class DtlsDatagramSocket extends DatagramSocket { + private static final Logger logger = LoggerFactory.getLogger(DtlsDatagramSocket.class); + private static final int HANDSHAKE_PACKET_SIZE = 1024; + + private final SSLContext sslContext; + private final SSLParameters sslParameters; + + private SSLEngine sslEngine; + + /** + * Initializes the datagram socket with the given DTLS context + * and parameters. The socket will be bound to an arbitrary + * free local port chosen by the operating system. + * + * @param sslContext the ssl context. + * @param sslParameters the ssl parameters. + * @exception SocketException if the socket could not be opened, + * or the socket could not bind to any local port. + * @exception SecurityException if a security manager exists and its + * {@code checkListen} method doesn't allow the operation. + */ + public DtlsDatagramSocket(SSLContext sslContext, + SSLParameters sslParameters) + throws SocketException { + this(sslContext, sslParameters, 0); + } + + /** + * Initializes the datagram socket with the given DTLS context + * and parameters. The socket will be bound to the given local + * port. + * + * @param sslContext the ssl context. + * @param sslParameters the ssl parameters. + * @param port the local port to bind to. + * @exception SocketException if the socket could not be opened, + * or the socket could not bind to the given port. + * @exception SecurityException if a security manager exists and its + * {@code checkListen} method doesn't allow the operation. + */ + public DtlsDatagramSocket(SSLContext sslContext, + SSLParameters sslParameters, int port) + throws SocketException { + super(port); + if (!"DTLS".equalsIgnoreCase(sslContext.getProtocol())) { + throw new IllegalArgumentException("SSLContext not for DTLS"); + } + this.sslContext = requireNonNull(sslContext); + this.sslParameters = requireNonNull(sslParameters); + } + + @Override + public void send(DatagramPacket packet) throws IOException { + // Force the use of connected ports to avoid juggling handshakes + // for different destinations. + if (!isConnected()) { + throw new IllegalStateException("Socket must be connected"); + } + + if (sslEngine == null) { + sslEngine = sslContext.createSSLEngine( + getInetAddress().getHostName(), getPort()); + sslEngine.setSSLParameters(sslParameters); + sslEngine.setUseClientMode(true); + + handshake(sslEngine, packet); + } + + if (sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING) { + throw new IllegalStateException("DTLS handshake failed"); + } + + var sendBuffer = ByteBuffer.wrap(packet.getData(), + packet.getOffset(), packet.getLength()); + var networkBuffer = ByteBuffer.allocate(16384); + var result = sslEngine.wrap(sendBuffer, networkBuffer); + if (result.getStatus() != Status.OK) { + throw new IOException("Error creating DTLS packet: " + + result); + } + + networkBuffer.flip(); + logger.debug("Sending packet, size={}", networkBuffer.remaining()); + DtlsPacketDebug.debug(networkBuffer); + var buffer = new byte[networkBuffer.remaining()]; + networkBuffer.get(buffer); + packet = new DatagramPacket(buffer, buffer.length, + packet.getSocketAddress()); + super.send(packet); + } + + /** + * Returns the SSL session object associated with this socket. + * + * @return the ssl session. + */ + public SSLSession getSession() { + return sslEngine == null ? null : sslEngine.getSession(); + } + + @Override + public synchronized void receive(DatagramPacket packet) throws IOException { + if (sslEngine == null) { + sslEngine = sslContext.createSSLEngine(); + sslEngine.setSSLParameters(sslParameters); + sslEngine.setUseClientMode(false); + + handshake(sslEngine, packet); + } + super.receive(packet); + logger.debug("Received packet, length={}", packet.getLength()); + + var network = ByteBuffer.wrap(packet.getData(), + packet.getOffset(), packet.getLength()); + var application = ByteBuffer.allocate(packet.getData().length); + var result = sslEngine.unwrap(network, application); + if (result.getStatus() != Status.OK) { + throw new IOException("DTLS error: " + result); + } + + var len = application.flip().remaining(); + application.get(packet.getData(), 0, + Math.min(len, packet.getData().length)); + packet.setLength(len); + } + + private void handshake(SSLEngine engine, DatagramPacket originalPacket) throws IOException { + logger.debug("Beginning DTLS handshake"); + engine.beginHandshake(); + + var netData = ByteBuffer.allocate(HANDSHAKE_PACKET_SIZE); + var appData = ByteBuffer.allocate(HANDSHAKE_PACKET_SIZE); + + var packet = new DatagramPacket(new byte[HANDSHAKE_PACKET_SIZE], + HANDSHAKE_PACKET_SIZE); + if (originalPacket.getPort() != -1) { + packet.setSocketAddress(originalPacket.getSocketAddress()); + } + + var status = engine.getHandshakeStatus(); + SSLEngineResult result; + while (status != HandshakeStatus.FINISHED) { + logger.debug("Handshake status: " + status); + switch (status) { + case NEED_UNWRAP: + super.receive(packet); + logger.debug("Packed received, size={}", packet.getLength()); + netData.put(packet.getData(), packet.getOffset(), packet.getLength()); + DtlsPacketDebug.debug(netData); + netData.flip(); + result = engine.unwrap(netData, appData); + netData.compact(); + logger.debug("Unwrap result: {}", result); + + if (result.getStatus() != Status.OK) { + throw new IllegalStateException("Unwrap failed: " + result); + } + + status = result.getHandshakeStatus(); + break; + case NEED_UNWRAP_AGAIN: + netData.flip(); + result = engine.unwrap(netData, appData); + netData.compact(); + logger.debug("Unwrap result: {}", result); + + if (result.getStatus() != Status.OK) { + throw new IllegalStateException("Unwrap failed: " + result); + } + + status = result.getHandshakeStatus(); + break; + case NEED_TASK: + Runnable task; + while ((task = engine.getDelegatedTask()) != null) { + logger.debug("Running task: {}", task); + task.run(); + } + status = engine.getHandshakeStatus(); + logger.debug("Tasks executed"); + break; + case NEED_WRAP: + result = engine.wrap(appData, netData); + logger.debug("Wrap result: " + result); + + if (result.getStatus() != Status.OK) { + throw new IllegalStateException("Wrap failed: " + result); + } + + netData.flip(); + DtlsPacketDebug.debug(netData); + + if (netData.hasRemaining()) { + var buffer = new byte[netData.remaining()]; + netData.get(buffer); + packet.setData(buffer); + logger.debug("Sending packet to: {}, size={}", + packet.getSocketAddress(), + packet.getLength()); + super.send(packet); + } + status = result.getHandshakeStatus(); + break; + default: + throw new IllegalStateException( + "Unexpected handshake state: " + status); + } + } + logger.debug("Handshake finished!"); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsPacketDebug.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsPacketDebug.java new file mode 100644 index 0000000..796f0af --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsPacketDebug.java @@ -0,0 +1,126 @@ +package com.manning.apisecurityinaction; + +import java.nio.*; + +import org.slf4j.*; + +/** + * Utility methods for debugging (D)TLS record layer and handshake + * messages. + */ +class DtlsPacketDebug { + private static final Logger logger = LoggerFactory.getLogger(DtlsPacketDebug.class); + + enum TlsVersion { + SSL_2_0(2, 0), + SSL_3_0(3, 0), + TLS_1_0(3, 1), + TLS_1_1(3, 2), + TLS_1_2(3, 3), // Or later, as TLS 1.3 reuses the same version + DTLS_1_0(254, 255), + DTLS_1_2(254, 253); + final byte major; + final byte minor; + + TlsVersion(int major, int minor) { + this.major = (byte) major; + this.minor = (byte) minor; + } + + static TlsVersion get(byte major, byte minor) { + for (var candidate : values()) { + if (candidate.major == major && candidate.minor == minor) + return candidate; + } + return null; + } + } + + enum ContentType { + CHANGE_CIPHER_SPEC(20), + ALERT(21), + HANDSHAKE(22), + APPLICATION_DATA(23); + + final byte value; + + ContentType(int value) { + this.value = (byte) value; + } + + static ContentType get(byte value) { + for (var type : values()) { + if (type.value == value) + return type; + } + return null; + } + } + + // Handshake message types + enum HandshakeMessageType { + HELLO_REQUEST(0), + CLIENT_HELLO(1), + SERVER_HELLO(2), + HELLO_VERIFY_REQUEST(3), // DTLS-specific + CERTIFICATE(11), + SERVER_KEY_EXCHANGE(12), + CERTIFICATE_REQUEST(13), + SERVER_HELLO_DONE(14), + CERTIFICATE_VERIFY(15), + CLIENT_KEY_EXCHANGE(16), + FINISHED(20) + ; + + final byte type; + + HandshakeMessageType(int type) { + this.type = (byte) type; + } + + static HandshakeMessageType get(byte value) { + for (var candidate : values()) { + if (candidate.type == value) { + return candidate; + } + } + return null; + } + } + + + static void debug(ByteBuffer data) { + var packet = data.duplicate().order(ByteOrder.BIG_ENDIAN); + + var info = new StringBuilder(); + + var packetType = ContentType.get(packet.get()); + info.append(packetType).append(" "); + + var protoMajor = packet.get(); + var protoMinor = packet.get(); + var version = TlsVersion.get(protoMajor, protoMinor); + info.append(version).append(" "); + + var epoch = packet.getShort() & 0xFFFF; + var sequence = ((packet.getInt() & 0xFFFFFFFFL) << 16) + | (packet.getShort() & 0xFFFFL); + var length = packet.getShort() & 0xFFFF; + + info.append("epoch=").append(epoch).append(", seq=") + .append(sequence).append(", len=").append(length) + .append(" "); + + if (packetType == ContentType.HANDSHAKE) { + var messageType = HandshakeMessageType.get(packet.get()); + info.append(messageType); + + var messageLen = ((packet.getShort() & 0xFFFF) << 8) + | (packet.get() & 0xFF); + + info.append(" (len=").append(messageLen).append(") "); + } + + logger.debug(info.toString()); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java new file mode 100644 index 0000000..9db5afd --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java @@ -0,0 +1,42 @@ +package com.manning.apisecurityinaction; + +import javax.net.ssl.*; +import java.io.FileInputStream; +import java.nio.ByteBuffer; +import java.security.KeyStore; + +public class DtlsServer { + static final int PACKET_SIZE = 1024; + + public static void main(String... args) throws Exception { + try (var channel = new DtlsDatagramChannel( + getServerContext(), DtlsClient.sslParameters())) { + channel.bind(54321); + System.out.println("Listening on port 54321"); + + var buffer = ByteBuffer.allocate(PACKET_SIZE); + + while (true) { + channel.receive(buffer); + buffer.flip(); + var data = buffer.asCharBuffer().toString(); + System.out.println("Data: " + data); + buffer.compact(); + } + } + } + + private static SSLContext getServerContext() throws Exception { + var sslContext = SSLContext.getInstance("DTLS"); + + var keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new FileInputStream("localhost.p12"), + "changeit".toCharArray()); + + var keyManager = KeyManagerFactory.getInstance("PKIX"); + keyManager.init(keyStore, "changeit".toCharArray()); + + sslContext.init(keyManager.getKeyManagers(), null, null); + return sslContext; + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java deleted file mode 100644 index 3f399b6..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/UdpServer.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.manning.apisecurityinaction; - -import java.io.FileInputStream; -import java.net.DatagramPacket; -import java.security.KeyStore; - -import javax.net.ssl.*; - -public class UdpServer { - static final int PACKET_SIZE = 1024; - - public static void main(String... args) throws Exception { - try (var socket = new DTLSDatagramSocket(getSslContext(), 54321)) { - System.out.printf("Listening on port %d%n", - socket.getLocalPort()); - - var buffer = new byte[PACKET_SIZE]; - - while (true) { - var packet = new DatagramPacket(buffer, PACKET_SIZE); - socket.receive(packet); - System.out.printf("Received packet from %s - len: %d%n", - packet.getSocketAddress(), packet.getLength()); - var message = new String(buffer, 0, packet.getLength()); - System.out.println("Message: " + message); - } - } - } - - private static SSLContext getSslContext() throws Exception { - var sslContext = SSLContext.getInstance("DTLS"); - - var keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(new FileInputStream("localhost.p12"), - "changeit".toCharArray()); - - var keyManager = KeyManagerFactory.getInstance( - KeyManagerFactory.getDefaultAlgorithm()); - keyManager.init(keyStore, "changeit".toCharArray()); - - sslContext.init(keyManager.getKeyManagers(), null, null); - return sslContext; - } -} From fdcd1e4012c07ad0621e84903ce06cf2284d1d68 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 5 Mar 2020 22:27:20 +0000 Subject: [PATCH 190/209] Debugged DTLS implementation --- .../apisecurityinaction/DtlsClient.java | 22 +- .../DtlsDatagramChannel.java | 139 ++++++++--- .../DtlsDatagramSocket.java | 227 ------------------ .../apisecurityinaction/DtlsPacketDebug.java | 126 ---------- .../apisecurityinaction/DtlsServer.java | 22 +- 5 files changed, 123 insertions(+), 413 deletions(-) delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramSocket.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DtlsPacketDebug.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java index f1d7d19..6f31243 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java @@ -7,20 +7,26 @@ import static java.nio.charset.StandardCharsets.UTF_8; +import org.slf4j.*; + public class DtlsClient { - public static void main(String... args) throws Exception { + private static final Logger logger = LoggerFactory.getLogger(DtlsClient.class); - try (var channel = new DtlsDatagramChannel(getClientContext(), sslParameters()); + public static void main(String... args) throws Exception { + try (var channel = new DtlsDatagramChannel(getClientContext()); var in = Files.newBufferedReader(Paths.get("test.txt"))) { + logger.info("Connecting to localhost:54321"); channel.connect("localhost", 54321); String line; while ((line = in.readLine()) != null) { - System.out.println("Sending packet to server"); + logger.info("Sending packet to server: {}", line); channel.send(line.getBytes(UTF_8)); } - System.out.println("All packets sent"); + logger.info("All packets sent"); + logger.info("Used cipher suite: {}", + channel.getSession().getCipherSuite()); } } @@ -40,12 +46,4 @@ private static SSLContext getClientContext() throws Exception { return sslContext; } - static SSLParameters sslParameters() { - var params = new SSLParameters(); - params.setProtocols(new String[] { "DTLSv1.2" }); - params.setMaximumPacketSize(1500); - params.setEnableRetransmissions(true); - params.setEndpointIdentificationAlgorithm("HTTPS"); - return params; - } } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java index 0de747f..94d6a1e 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java @@ -1,14 +1,26 @@ package com.manning.apisecurityinaction; -import javax.net.ssl.*; -import javax.net.ssl.SSLEngineResult.*; import java.io.*; import java.net.*; import java.nio.*; -import java.nio.channels.*; +import java.nio.channels.DatagramChannel; + +import javax.net.ssl.*; +import javax.net.ssl.SSLEngineResult.*; import org.slf4j.*; +/** + * A rudimentary wrapper around the {@link SSLEngine} low-level DTLS + * protocol state machine. + *

+ * Note: this class doesn't attempt to handle timeouts, + * lost packets, retransmissions, buffer overflows, and many other details + * of a robust UDP-based protocol implementation. It implements enough + * to provide a guidance to DTLS usage in Java. When used as a server, + * this class only supports a single client at a time and will discard + * packets received from other concurrent clients. + */ public class DtlsDatagramChannel implements Closeable { private static final Logger logger = LoggerFactory.getLogger(DtlsDatagramChannel.class); @@ -16,9 +28,9 @@ public class DtlsDatagramChannel implements Closeable { private final SSLContext sslContext; private final SSLParameters sslParameters; - private final ByteBuffer netRecvBuffer; - private final ByteBuffer netSendBuffer; - private final ByteBuffer appBuffer; + private ByteBuffer netRecvBuffer; + private ByteBuffer netSendBuffer; + private ByteBuffer appBuffer; private SSLEngine sslEngine; @@ -27,17 +39,35 @@ public DtlsDatagramChannel(SSLContext sslContext, SSLParameters sslParameters) t this.sslContext = sslContext; this.sslParameters = sslParameters; - this.netRecvBuffer = ByteBuffer.allocate(2048); - this.netSendBuffer = ByteBuffer.allocate(65535); - this.appBuffer = ByteBuffer.allocate(2048); + this.netRecvBuffer = ByteBuffer.allocateDirect(2048); } - public void bind(int port) throws IOException { - channel.bind(new InetSocketAddress(InetAddress.getLocalHost(), port)); + public DtlsDatagramChannel(SSLContext sslContext) throws IOException { + this(sslContext, defaultSslParameters()); } - public void connect(String host, int port) throws IOException { + public static SSLParameters defaultSslParameters() { + var params = new SSLParameters(); + params.setProtocols(new String[] { "DTLSv1.2" }); + params.setMaximumPacketSize(1500); + params.setEnableRetransmissions(true); + params.setEndpointIdentificationAlgorithm("HTTPS"); + return params; + } + + + public DtlsDatagramChannel bind(int port) throws IOException { + channel.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), port)); + return this; + } + + public DtlsDatagramChannel connect(String host, int port) throws IOException { channel.connect(new InetSocketAddress(host, port)); + return this; + } + + public SSLSession getSession() { + return sslEngine.getSession(); } public void send(byte[] data) throws IOException { @@ -83,9 +113,9 @@ public InetSocketAddress receive(ByteBuffer buffer) throws IOException { sslEngine.setSSLParameters(sslParameters); handshake(sslEngine); + channel.receive(netRecvBuffer); } - channel.receive(netRecvBuffer); netRecvBuffer.flip(); var result = sslEngine.unwrap(netRecvBuffer, buffer); netRecvBuffer.compact(); @@ -97,7 +127,12 @@ public InetSocketAddress receive(ByteBuffer buffer) throws IOException { throw new BufferOverflowException(); } if (result.getStatus() == Status.CLOSED) { - throw new ClosedChannelException(); + logger.info("Client disconnected"); + sslEngine.closeInbound(); + processEngineLoop(sslEngine); + sslEngine.closeOutbound(); + channel.disconnect(); + sslEngine = null; } return address; @@ -106,51 +141,69 @@ public InetSocketAddress receive(ByteBuffer buffer) throws IOException { @Override public void close() throws IOException { sslEngine.closeOutbound(); + // We should be able to call processEngineLoop here, but in OpenJDK 13 + // it erroneously returns HANDSHAKE_DONE when we are still waiting for + // the other side's close_notify alert. + + // Send close_notify alert + appBuffer.flip(); + sslEngine.wrap(appBuffer, netSendBuffer); + appBuffer.compact(); + netSendBuffer.flip(); + if (netSendBuffer.hasRemaining()) { + channel.write(netSendBuffer); + netSendBuffer.compact(); + } + + // Wait for close_notify response + while (!sslEngine.isInboundDone()) { + channel.receive(netRecvBuffer); + netRecvBuffer.flip(); + sslEngine.unwrap(netRecvBuffer, appBuffer); + netRecvBuffer.compact(); + } sslEngine.closeInbound(); + channel.close(); } private void handshake(SSLEngine engine) throws IOException { if (!channel.isConnected()) { throw new IllegalStateException("Channel must be connected"); } - logger.debug("Beginning DTLS handshake"); + logger.info("Beginning DTLS handshake"); engine.beginHandshake(); + appBuffer = ByteBuffer.allocateDirect(engine.getSession().getApplicationBufferSize()); + netSendBuffer = ByteBuffer.allocateDirect(engine.getSession().getPacketBufferSize()); + + processEngineLoop(engine); + } + + private void processEngineLoop(SSLEngine engine) throws IOException { var status = engine.getHandshakeStatus(); - SSLEngineResult result; - while (status != HandshakeStatus.FINISHED) { + while (status != HandshakeStatus.FINISHED && status != HandshakeStatus.NOT_HANDSHAKING) { + SSLEngineResult result; logger.debug("Handshake status: " + status); switch (status) { case NEED_UNWRAP: + if (netRecvBuffer.position() == 0) { + channel.receive(netRecvBuffer); + } + case NEED_UNWRAP_AGAIN: netRecvBuffer.flip(); result = engine.unwrap(netRecvBuffer, appBuffer); netRecvBuffer.compact(); logger.debug("Unwrap result: {}", result); - if (result.getStatus() == Status.BUFFER_UNDERFLOW) { + while (result.getStatus() == Status.BUFFER_UNDERFLOW) { + netRecvBuffer = ensureCapacity(netRecvBuffer, + sslEngine.getSession().getPacketBufferSize()); channel.receive(netRecvBuffer); netRecvBuffer.flip(); - DtlsPacketDebug.debug(netRecvBuffer); result = engine.unwrap(netRecvBuffer, appBuffer); netRecvBuffer.compact(); } - if (result.getStatus() != Status.OK) { - throw new IllegalStateException("Unwrap failed: " + result); - } - - status = result.getHandshakeStatus(); - break; - case NEED_UNWRAP_AGAIN: - netRecvBuffer.flip(); - result = engine.unwrap(netRecvBuffer, appBuffer); - netRecvBuffer.compact(); - logger.debug("Unwrap result: {}", result); - - if (result.getStatus() != Status.OK) { - throw new IllegalStateException("Unwrap failed: " + result); - } - status = result.getHandshakeStatus(); break; case NEED_TASK: @@ -163,16 +216,12 @@ private void handshake(SSLEngine engine) throws IOException { logger.debug("Tasks executed"); break; case NEED_WRAP: + appBuffer.flip(); result = engine.wrap(appBuffer, netSendBuffer); appBuffer.compact(); logger.debug("Wrap result: " + result); - if (result.getStatus() != Status.OK) { - throw new IllegalStateException("Wrap failed: " + result); - } - netSendBuffer.flip(); - DtlsPacketDebug.debug(netSendBuffer); if (netSendBuffer.hasRemaining()) { channel.write(netSendBuffer); netSendBuffer.compact(); @@ -186,4 +235,14 @@ private void handshake(SSLEngine engine) throws IOException { } logger.debug("Handshake finished!"); } + + private ByteBuffer ensureCapacity(ByteBuffer buffer, int requiredSize) { + var remaining = buffer.remaining(); + if (remaining < requiredSize) { + var newBuffer = ByteBuffer.allocate(buffer.position() + requiredSize); + newBuffer.put(buffer.flip()); + return newBuffer; + } + return buffer; + } } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramSocket.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramSocket.java deleted file mode 100644 index 6e616b4..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramSocket.java +++ /dev/null @@ -1,227 +0,0 @@ -package com.manning.apisecurityinaction; - -import javax.net.ssl.*; -import javax.net.ssl.SSLEngineResult.*; -import java.io.IOException; -import java.net.*; -import java.nio.ByteBuffer; - -import org.slf4j.*; - -import static java.util.Objects.requireNonNull; - -/** - * A wrapper over the UDP {@link DatagramSocket} that provides support - * for DTLS transport security. A DTLS handshake will be performed - */ -public class DtlsDatagramSocket extends DatagramSocket { - private static final Logger logger = LoggerFactory.getLogger(DtlsDatagramSocket.class); - private static final int HANDSHAKE_PACKET_SIZE = 1024; - - private final SSLContext sslContext; - private final SSLParameters sslParameters; - - private SSLEngine sslEngine; - - /** - * Initializes the datagram socket with the given DTLS context - * and parameters. The socket will be bound to an arbitrary - * free local port chosen by the operating system. - * - * @param sslContext the ssl context. - * @param sslParameters the ssl parameters. - * @exception SocketException if the socket could not be opened, - * or the socket could not bind to any local port. - * @exception SecurityException if a security manager exists and its - * {@code checkListen} method doesn't allow the operation. - */ - public DtlsDatagramSocket(SSLContext sslContext, - SSLParameters sslParameters) - throws SocketException { - this(sslContext, sslParameters, 0); - } - - /** - * Initializes the datagram socket with the given DTLS context - * and parameters. The socket will be bound to the given local - * port. - * - * @param sslContext the ssl context. - * @param sslParameters the ssl parameters. - * @param port the local port to bind to. - * @exception SocketException if the socket could not be opened, - * or the socket could not bind to the given port. - * @exception SecurityException if a security manager exists and its - * {@code checkListen} method doesn't allow the operation. - */ - public DtlsDatagramSocket(SSLContext sslContext, - SSLParameters sslParameters, int port) - throws SocketException { - super(port); - if (!"DTLS".equalsIgnoreCase(sslContext.getProtocol())) { - throw new IllegalArgumentException("SSLContext not for DTLS"); - } - this.sslContext = requireNonNull(sslContext); - this.sslParameters = requireNonNull(sslParameters); - } - - @Override - public void send(DatagramPacket packet) throws IOException { - // Force the use of connected ports to avoid juggling handshakes - // for different destinations. - if (!isConnected()) { - throw new IllegalStateException("Socket must be connected"); - } - - if (sslEngine == null) { - sslEngine = sslContext.createSSLEngine( - getInetAddress().getHostName(), getPort()); - sslEngine.setSSLParameters(sslParameters); - sslEngine.setUseClientMode(true); - - handshake(sslEngine, packet); - } - - if (sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING) { - throw new IllegalStateException("DTLS handshake failed"); - } - - var sendBuffer = ByteBuffer.wrap(packet.getData(), - packet.getOffset(), packet.getLength()); - var networkBuffer = ByteBuffer.allocate(16384); - var result = sslEngine.wrap(sendBuffer, networkBuffer); - if (result.getStatus() != Status.OK) { - throw new IOException("Error creating DTLS packet: " + - result); - } - - networkBuffer.flip(); - logger.debug("Sending packet, size={}", networkBuffer.remaining()); - DtlsPacketDebug.debug(networkBuffer); - var buffer = new byte[networkBuffer.remaining()]; - networkBuffer.get(buffer); - packet = new DatagramPacket(buffer, buffer.length, - packet.getSocketAddress()); - super.send(packet); - } - - /** - * Returns the SSL session object associated with this socket. - * - * @return the ssl session. - */ - public SSLSession getSession() { - return sslEngine == null ? null : sslEngine.getSession(); - } - - @Override - public synchronized void receive(DatagramPacket packet) throws IOException { - if (sslEngine == null) { - sslEngine = sslContext.createSSLEngine(); - sslEngine.setSSLParameters(sslParameters); - sslEngine.setUseClientMode(false); - - handshake(sslEngine, packet); - } - super.receive(packet); - logger.debug("Received packet, length={}", packet.getLength()); - - var network = ByteBuffer.wrap(packet.getData(), - packet.getOffset(), packet.getLength()); - var application = ByteBuffer.allocate(packet.getData().length); - var result = sslEngine.unwrap(network, application); - if (result.getStatus() != Status.OK) { - throw new IOException("DTLS error: " + result); - } - - var len = application.flip().remaining(); - application.get(packet.getData(), 0, - Math.min(len, packet.getData().length)); - packet.setLength(len); - } - - private void handshake(SSLEngine engine, DatagramPacket originalPacket) throws IOException { - logger.debug("Beginning DTLS handshake"); - engine.beginHandshake(); - - var netData = ByteBuffer.allocate(HANDSHAKE_PACKET_SIZE); - var appData = ByteBuffer.allocate(HANDSHAKE_PACKET_SIZE); - - var packet = new DatagramPacket(new byte[HANDSHAKE_PACKET_SIZE], - HANDSHAKE_PACKET_SIZE); - if (originalPacket.getPort() != -1) { - packet.setSocketAddress(originalPacket.getSocketAddress()); - } - - var status = engine.getHandshakeStatus(); - SSLEngineResult result; - while (status != HandshakeStatus.FINISHED) { - logger.debug("Handshake status: " + status); - switch (status) { - case NEED_UNWRAP: - super.receive(packet); - logger.debug("Packed received, size={}", packet.getLength()); - netData.put(packet.getData(), packet.getOffset(), packet.getLength()); - DtlsPacketDebug.debug(netData); - netData.flip(); - result = engine.unwrap(netData, appData); - netData.compact(); - logger.debug("Unwrap result: {}", result); - - if (result.getStatus() != Status.OK) { - throw new IllegalStateException("Unwrap failed: " + result); - } - - status = result.getHandshakeStatus(); - break; - case NEED_UNWRAP_AGAIN: - netData.flip(); - result = engine.unwrap(netData, appData); - netData.compact(); - logger.debug("Unwrap result: {}", result); - - if (result.getStatus() != Status.OK) { - throw new IllegalStateException("Unwrap failed: " + result); - } - - status = result.getHandshakeStatus(); - break; - case NEED_TASK: - Runnable task; - while ((task = engine.getDelegatedTask()) != null) { - logger.debug("Running task: {}", task); - task.run(); - } - status = engine.getHandshakeStatus(); - logger.debug("Tasks executed"); - break; - case NEED_WRAP: - result = engine.wrap(appData, netData); - logger.debug("Wrap result: " + result); - - if (result.getStatus() != Status.OK) { - throw new IllegalStateException("Wrap failed: " + result); - } - - netData.flip(); - DtlsPacketDebug.debug(netData); - - if (netData.hasRemaining()) { - var buffer = new byte[netData.remaining()]; - netData.get(buffer); - packet.setData(buffer); - logger.debug("Sending packet to: {}, size={}", - packet.getSocketAddress(), - packet.getLength()); - super.send(packet); - } - status = result.getHandshakeStatus(); - break; - default: - throw new IllegalStateException( - "Unexpected handshake state: " + status); - } - } - logger.debug("Handshake finished!"); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsPacketDebug.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsPacketDebug.java deleted file mode 100644 index 796f0af..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsPacketDebug.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.manning.apisecurityinaction; - -import java.nio.*; - -import org.slf4j.*; - -/** - * Utility methods for debugging (D)TLS record layer and handshake - * messages. - */ -class DtlsPacketDebug { - private static final Logger logger = LoggerFactory.getLogger(DtlsPacketDebug.class); - - enum TlsVersion { - SSL_2_0(2, 0), - SSL_3_0(3, 0), - TLS_1_0(3, 1), - TLS_1_1(3, 2), - TLS_1_2(3, 3), // Or later, as TLS 1.3 reuses the same version - DTLS_1_0(254, 255), - DTLS_1_2(254, 253); - final byte major; - final byte minor; - - TlsVersion(int major, int minor) { - this.major = (byte) major; - this.minor = (byte) minor; - } - - static TlsVersion get(byte major, byte minor) { - for (var candidate : values()) { - if (candidate.major == major && candidate.minor == minor) - return candidate; - } - return null; - } - } - - enum ContentType { - CHANGE_CIPHER_SPEC(20), - ALERT(21), - HANDSHAKE(22), - APPLICATION_DATA(23); - - final byte value; - - ContentType(int value) { - this.value = (byte) value; - } - - static ContentType get(byte value) { - for (var type : values()) { - if (type.value == value) - return type; - } - return null; - } - } - - // Handshake message types - enum HandshakeMessageType { - HELLO_REQUEST(0), - CLIENT_HELLO(1), - SERVER_HELLO(2), - HELLO_VERIFY_REQUEST(3), // DTLS-specific - CERTIFICATE(11), - SERVER_KEY_EXCHANGE(12), - CERTIFICATE_REQUEST(13), - SERVER_HELLO_DONE(14), - CERTIFICATE_VERIFY(15), - CLIENT_KEY_EXCHANGE(16), - FINISHED(20) - ; - - final byte type; - - HandshakeMessageType(int type) { - this.type = (byte) type; - } - - static HandshakeMessageType get(byte value) { - for (var candidate : values()) { - if (candidate.type == value) { - return candidate; - } - } - return null; - } - } - - - static void debug(ByteBuffer data) { - var packet = data.duplicate().order(ByteOrder.BIG_ENDIAN); - - var info = new StringBuilder(); - - var packetType = ContentType.get(packet.get()); - info.append(packetType).append(" "); - - var protoMajor = packet.get(); - var protoMinor = packet.get(); - var version = TlsVersion.get(protoMajor, protoMinor); - info.append(version).append(" "); - - var epoch = packet.getShort() & 0xFFFF; - var sequence = ((packet.getInt() & 0xFFFFFFFFL) << 16) - | (packet.getShort() & 0xFFFFL); - var length = packet.getShort() & 0xFFFF; - - info.append("epoch=").append(epoch).append(", seq=") - .append(sequence).append(", len=").append(length) - .append(" "); - - if (packetType == ContentType.HANDSHAKE) { - var messageType = HandshakeMessageType.get(packet.get()); - info.append(messageType); - - var messageLen = ((packet.getShort() & 0xFFFF) << 8) - | (packet.get() & 0xFF); - - info.append(" (len=").append(messageLen).append(") "); - } - - logger.debug(info.toString()); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java index 9db5afd..ec8f50b 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java @@ -1,28 +1,34 @@ package com.manning.apisecurityinaction; -import javax.net.ssl.*; import java.io.FileInputStream; import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.charset.StandardCharsets; import java.security.KeyStore; +import javax.net.ssl.*; + +import org.slf4j.*; + public class DtlsServer { - static final int PACKET_SIZE = 1024; + private static final Logger logger = LoggerFactory.getLogger(DtlsServer.class); public static void main(String... args) throws Exception { - try (var channel = new DtlsDatagramChannel( - getServerContext(), DtlsClient.sslParameters())) { + try (var channel = new DtlsDatagramChannel(getServerContext())) { channel.bind(54321); - System.out.println("Listening on port 54321"); + logger.info("Listening on port 54321"); - var buffer = ByteBuffer.allocate(PACKET_SIZE); + var buffer = ByteBuffer.allocate(2048); while (true) { channel.receive(buffer); buffer.flip(); - var data = buffer.asCharBuffer().toString(); - System.out.println("Data: " + data); + var data = StandardCharsets.UTF_8.decode(buffer).toString(); + logger.info("Received: {}", data); buffer.compact(); } + } catch (ClosedChannelException e) { + logger.info("Client disconnected"); } } From 6b84116a57c1eccc7f0b7c52d1b2655d507f8d09 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 9 Mar 2020 11:56:58 +0000 Subject: [PATCH 191/209] Use ChaCha20-Poly1305 cipher suites --- .../apisecurityinaction/DtlsClient.java | 18 +++++++++++++++--- .../apisecurityinaction/DtlsServer.java | 8 +++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java index 6f31243..9ec220d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java @@ -1,11 +1,12 @@ package com.manning.apisecurityinaction; -import javax.net.ssl.*; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.FileInputStream; import java.nio.file.*; import java.security.KeyStore; -import static java.nio.charset.StandardCharsets.UTF_8; +import javax.net.ssl.*; import org.slf4j.*; @@ -13,7 +14,7 @@ public class DtlsClient { private static final Logger logger = LoggerFactory.getLogger(DtlsClient.class); public static void main(String... args) throws Exception { - try (var channel = new DtlsDatagramChannel(getClientContext()); + try (var channel = new DtlsDatagramChannel(getClientContext(), sslParameters()); var in = Files.newBufferedReader(Paths.get("test.txt"))) { logger.info("Connecting to localhost:54321"); channel.connect("localhost", 54321); @@ -46,4 +47,15 @@ private static SSLContext getClientContext() throws Exception { return sslContext; } + private static SSLParameters sslParameters() { + var params = DtlsDatagramChannel.defaultSslParameters(); + params.setCipherSuites(new String[] { + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_EMPTY_RENEGOTIATION_INFO_SCSV" + }); + return params; + } + } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java index ec8f50b..53770aa 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java @@ -1,9 +1,9 @@ package com.manning.apisecurityinaction; +import static java.nio.charset.StandardCharsets.UTF_8; + import java.io.FileInputStream; import java.nio.ByteBuffer; -import java.nio.channels.ClosedChannelException; -import java.nio.charset.StandardCharsets; import java.security.KeyStore; import javax.net.ssl.*; @@ -23,12 +23,10 @@ public static void main(String... args) throws Exception { while (true) { channel.receive(buffer); buffer.flip(); - var data = StandardCharsets.UTF_8.decode(buffer).toString(); + var data = UTF_8.decode(buffer).toString(); logger.info("Received: {}", data); buffer.compact(); } - } catch (ClosedChannelException e) { - logger.info("Client disconnected"); } } From 18f6c5c9c266845195289a49b31f93a116e5ba35 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 9 Mar 2020 14:15:56 +0000 Subject: [PATCH 192/209] BouncyCastle TLS PSK example --- natter-api/pom.xml | 5 ++ .../apisecurityinaction/PskClient.java | 41 +++++++++++++ .../apisecurityinaction/PskServer.java | 59 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java diff --git a/natter-api/pom.xml b/natter-api/pom.xml index 55846f3..e4607c6 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -60,6 +60,11 @@ bcpkix-jdk15on 1.64 + + org.bouncycastle + bctls-jdk15on + 1.64 + software.pando.crypto salty-coffee diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java new file mode 100644 index 0000000..932c67b --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java @@ -0,0 +1,41 @@ +package com.manning.apisecurityinaction; + +import java.io.PrintStream; +import java.net.Socket; +import java.nio.file.*; +import java.security.SecureRandom; + +import org.bouncycastle.tls.*; +import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; + +import software.pando.crypto.nacl.Crypto; + +public class PskClient { + + public static void main(String[] args) throws Exception { + var psk = PskServer.loadPsk(); + var pskId = Crypto.hash(psk); + + var crypto = new BcTlsCrypto(new SecureRandom()); + var client = new PSKTlsClient(crypto, pskId, psk) { + @Override + protected int[] getSupportedCipherSuites() { + return new int[] { + CipherSuite.TLS_PSK_WITH_AES_128_CCM + }; + } + }; + + var socket = new Socket("localhost", 54321); + + var protocol = new TlsClientProtocol(socket.getInputStream(), + socket.getOutputStream()); + protocol.connect(client); + + try (var out = new PrintStream(protocol.getOutputStream()); + var in = Files.newBufferedReader(Paths.get("test.txt"))) { + + in.lines().forEach(out::println); + } + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java new file mode 100644 index 0000000..05cbcb6 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java @@ -0,0 +1,59 @@ +package com.manning.apisecurityinaction; + +import java.io.*; +import java.net.ServerSocket; +import java.security.*; + +import org.bouncycastle.tls.*; +import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; + +import software.pando.crypto.nacl.Crypto; + +public class PskServer { + public static void main(String[] args) throws Exception { + var psk = loadPsk(); + var pskId = Crypto.hash(psk); + + var crypto = new BcTlsCrypto(new SecureRandom()); + var server = new PSKTlsServer(crypto, new TlsPSKIdentityManager() { + @Override + public byte[] getHint() { + return pskId; + } + + @Override + public byte[] getPSK(byte[] identity) { + return psk; + } + }) { + @Override + protected int[] getSupportedCipherSuites() { + return new int[] { + CipherSuite.TLS_PSK_WITH_AES_128_CCM + }; + } + }; + + var serverSocket = new ServerSocket(54321); + var socket = serverSocket.accept(); + var protocol = new TlsServerProtocol( + socket.getInputStream(), socket.getOutputStream()); + protocol.accept(server); + + try (var in = new BufferedReader(new InputStreamReader(protocol.getInputStream()))) { + String line; + while ((line = in.readLine()) != null) { + System.out.println("Received: " + line); + } + } + protocol.close(); + } + + static byte[] loadPsk() throws Exception { + var keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new FileInputStream("keystore.p12"), + "changeit".toCharArray()); + + return keyStore.getKey("aes-key", "changeit".toCharArray()).getEncoded(); + } +} From 4b5b8ec6dc43a025cbc71d2281bec0e2b4b8774e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 9 Mar 2020 14:54:21 +0000 Subject: [PATCH 193/209] Convert BC PSK example to use DTLS --- .../apisecurityinaction/PskClient.java | 32 ++++++++------- .../apisecurityinaction/PskServer.java | 40 +++++++++---------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java index 932c67b..b369628 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java @@ -1,7 +1,8 @@ package com.manning.apisecurityinaction; -import java.io.PrintStream; -import java.net.Socket; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.net.*; import java.nio.file.*; import java.security.SecureRandom; @@ -19,23 +20,26 @@ public static void main(String[] args) throws Exception { var crypto = new BcTlsCrypto(new SecureRandom()); var client = new PSKTlsClient(crypto, pskId, psk) { @Override - protected int[] getSupportedCipherSuites() { - return new int[] { - CipherSuite.TLS_PSK_WITH_AES_128_CCM - }; + protected ProtocolVersion[] getSupportedVersions() { + return ProtocolVersion.DTLSv12.only(); } }; - var socket = new Socket("localhost", 54321); - - var protocol = new TlsClientProtocol(socket.getInputStream(), - socket.getOutputStream()); - protocol.connect(client); + var address = InetAddress.getByName("localhost"); + var socket = new DatagramSocket(); + socket.connect(address, 54321); - try (var out = new PrintStream(protocol.getOutputStream()); - var in = Files.newBufferedReader(Paths.get("test.txt"))) { + var transport = new UDPTransport(socket, 1500); + var protocol = new DTLSClientProtocol(); + var dtls = protocol.connect(client, transport); - in.lines().forEach(out::println); + try (var in = Files.newBufferedReader(Paths.get("test.txt"))) { + String line; + while ((line = in.readLine()) != null) { + System.out.println("Sending: " + line); + var buf = line.getBytes(UTF_8); + dtls.send(buf, 0, buf.length); + } } } } diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java index 05cbcb6..a43aeb8 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java @@ -1,7 +1,9 @@ package com.manning.apisecurityinaction; -import java.io.*; -import java.net.ServerSocket; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.FileInputStream; +import java.net.*; import java.security.*; import org.bouncycastle.tls.*; @@ -20,33 +22,31 @@ public static void main(String[] args) throws Exception { public byte[] getHint() { return pskId; } - @Override public byte[] getPSK(byte[] identity) { return psk; } }) { @Override - protected int[] getSupportedCipherSuites() { - return new int[] { - CipherSuite.TLS_PSK_WITH_AES_128_CCM - }; + protected ProtocolVersion[] getSupportedVersions() { + return ProtocolVersion.DTLSv12.only(); } }; - - var serverSocket = new ServerSocket(54321); - var socket = serverSocket.accept(); - var protocol = new TlsServerProtocol( - socket.getInputStream(), socket.getOutputStream()); - protocol.accept(server); - - try (var in = new BufferedReader(new InputStreamReader(protocol.getInputStream()))) { - String line; - while ((line = in.readLine()) != null) { - System.out.println("Received: " + line); - } + var buffer = new byte[2048]; + var serverSocket = new DatagramSocket(54321); + var packet = new DatagramPacket(buffer, buffer.length); + serverSocket.receive(packet); + serverSocket.connect(packet.getSocketAddress()); + + var protocol = new DTLSServerProtocol(); + var transport = new UDPTransport(serverSocket, 1500); + var dtls = protocol.accept(server, transport); + + while (true) { + var len = dtls.receive(buffer, 0, buffer.length, 60000); + var data = new String(buffer, 0, len, UTF_8); + System.out.println("Received: " + data); } - protocol.close(); } static byte[] loadPsk() throws Exception { From 2f6cbf6bc4f443803b4db6e30c294a2a4bdec3a5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 9 Mar 2020 16:22:44 +0000 Subject: [PATCH 194/209] Send an empty initial packet to kick-start the handshake --- .../apisecurityinaction/PskClient.java | 8 +--- .../apisecurityinaction/PskServer.java | 41 +++++++++---------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java index b369628..42f437b 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java @@ -1,20 +1,16 @@ package com.manning.apisecurityinaction; import static java.nio.charset.StandardCharsets.UTF_8; - import java.net.*; import java.nio.file.*; import java.security.SecureRandom; - import org.bouncycastle.tls.*; import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; - import software.pando.crypto.nacl.Crypto; public class PskClient { - public static void main(String[] args) throws Exception { - var psk = PskServer.loadPsk(); + var psk = PskServer.loadPsk(args[0].toCharArray()); var pskId = Crypto.hash(psk); var crypto = new BcTlsCrypto(new SecureRandom()); @@ -28,7 +24,7 @@ protected ProtocolVersion[] getSupportedVersions() { var address = InetAddress.getByName("localhost"); var socket = new DatagramSocket(); socket.connect(address, 54321); - + socket.send(new DatagramPacket(new byte[0], 0)); var transport = new UDPTransport(socket, 1500); var protocol = new DTLSClientProtocol(); var dtls = protocol.connect(client, transport); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java index a43aeb8..0329b4e 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java @@ -1,32 +1,17 @@ package com.manning.apisecurityinaction; import static java.nio.charset.StandardCharsets.UTF_8; - import java.io.FileInputStream; import java.net.*; import java.security.*; - import org.bouncycastle.tls.*; import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; -import software.pando.crypto.nacl.Crypto; - public class PskServer { public static void main(String[] args) throws Exception { - var psk = loadPsk(); - var pskId = Crypto.hash(psk); - + var psk = loadPsk(args[0].toCharArray()); var crypto = new BcTlsCrypto(new SecureRandom()); - var server = new PSKTlsServer(crypto, new TlsPSKIdentityManager() { - @Override - public byte[] getHint() { - return pskId; - } - @Override - public byte[] getPSK(byte[] identity) { - return psk; - } - }) { + var server = new PSKTlsServer(crypto, getIdentityManager(psk)) { @Override protected ProtocolVersion[] getSupportedVersions() { return ProtocolVersion.DTLSv12.only(); @@ -49,11 +34,23 @@ protected ProtocolVersion[] getSupportedVersions() { } } - static byte[] loadPsk() throws Exception { - var keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(new FileInputStream("keystore.p12"), - "changeit".toCharArray()); + static TlsPSKIdentityManager getIdentityManager(byte[] psk) { + return new TlsPSKIdentityManager() { + @Override + public byte[] getHint() { + return null; + } - return keyStore.getKey("aes-key", "changeit".toCharArray()).getEncoded(); + @Override + public byte[] getPSK(byte[] identity) { + return psk; + } + }; + } + + static byte[] loadPsk(char[] password) throws Exception { + var keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(new FileInputStream("keystore.p12"), password); + return keyStore.getKey("aes-key", password).getEncoded(); } } From c7fdb87fafbf9f3b36e3e1db115646fd9488171a Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 9 Mar 2020 17:13:46 +0000 Subject: [PATCH 195/209] Use raw PSK cipher suites --- .../com/manning/apisecurityinaction/PskClient.java | 10 ++++++++++ .../com/manning/apisecurityinaction/PskServer.java | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java index 42f437b..05d4a53 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java @@ -1,11 +1,14 @@ package com.manning.apisecurityinaction; import static java.nio.charset.StandardCharsets.UTF_8; + import java.net.*; import java.nio.file.*; import java.security.SecureRandom; + import org.bouncycastle.tls.*; import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; + import software.pando.crypto.nacl.Crypto; public class PskClient { @@ -19,6 +22,13 @@ public static void main(String[] args) throws Exception { protected ProtocolVersion[] getSupportedVersions() { return ProtocolVersion.DTLSv12.only(); } + + @Override + protected int[] getSupportedCipherSuites() { + return new int[] { + CipherSuite.TLS_PSK_WITH_AES_128_CCM + }; + } }; var address = InetAddress.getByName("localhost"); diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java index 0329b4e..c08b90a 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java @@ -16,6 +16,18 @@ public static void main(String[] args) throws Exception { protected ProtocolVersion[] getSupportedVersions() { return ProtocolVersion.DTLSv12.only(); } + @Override + protected int[] getSupportedCipherSuites() { + return new int[] { + CipherSuite.TLS_PSK_WITH_AES_128_CCM, + CipherSuite.TLS_PSK_WITH_AES_128_CCM_8, + CipherSuite.TLS_PSK_WITH_AES_256_CCM, + CipherSuite.TLS_PSK_WITH_AES_256_CCM_8, + CipherSuite.TLS_PSK_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_PSK_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_PSK_WITH_CHACHA20_POLY1305_SHA256 + }; + } }; var buffer = new byte[2048]; var serverSocket = new DatagramSocket(54321); @@ -29,6 +41,7 @@ protected ProtocolVersion[] getSupportedVersions() { while (true) { var len = dtls.receive(buffer, 0, buffer.length, 60000); + if (len == -1) break; var data = new String(buffer, 0, len, UTF_8); System.out.println("Received: " + data); } From 9984d6c4e135b1178d14adae4d9d5274ea089f28 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 12 Mar 2020 15:52:33 +0000 Subject: [PATCH 196/209] Add example of COSE encryption with HKDF --- natter-api/pom.xml | 5 +++ .../CoseEncryptionExample.java | 41 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java diff --git a/natter-api/pom.xml b/natter-api/pom.xml index e4607c6..3d931be 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -75,6 +75,11 @@ jmacaroons 0.4.1 + + com.augustcellars.cose + cose-java + 1.1.0 + diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java new file mode 100644 index 0000000..c501587 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java @@ -0,0 +1,41 @@ +package com.manning.apisecurityinaction; + +import COSE.*; +import com.manning.apisecurityinaction.token.Base64url; +import com.upokecenter.cbor.CBORObject; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.security.*; + +public class CoseEncryptionExample { + private static final SecureRandom random = new SecureRandom(); + + public static void main(String... args) throws Exception { + Security.addProvider(new BouncyCastleProvider()); + var keyMaterial = PskServer.loadPsk("changeit".toCharArray()); + + var recipient = new Recipient(); + var keyData = CBORObject.NewMap() + .Add(KeyKeys.KeyType.AsCBOR(), KeyKeys.KeyType_Octet) + .Add(KeyKeys.Octet_K.AsCBOR(), keyMaterial); + recipient.SetKey(new OneKey(keyData)); + recipient.addAttribute(HeaderKeys.Algorithm, + AlgorithmID.HKDF_HMAC_SHA_256.AsCBOR(), + Attribute.PROTECTED); + var nonce = new byte[16]; + random.nextBytes(nonce); + recipient.addAttribute(HeaderKeys.HKDF_Context_PartyU_nonce, + CBORObject.FromObject(nonce), Attribute.PROTECTED); + + var message = new EncryptMessage(); + message.SetContent("Hello, World!"); + message.addAttribute(HeaderKeys.Algorithm, + AlgorithmID.AES_CCM_16_128_128.AsCBOR(), + Attribute.PROTECTED); + message.addRecipient(recipient); + + message.encrypt(); + System.out.println(Base64url.encode(message.EncodeToBytes())); + System.out.println(message.EncodeToCBORObject()); + } +} From 319b9c40fdb96fcbf96e19dae2db3be89cee04e5 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 13 Mar 2020 15:54:03 +0000 Subject: [PATCH 197/209] Add decryption example --- .../apisecurityinaction/CoseEncryptionExample.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java index c501587..04bda27 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java @@ -7,6 +7,8 @@ import java.security.*; +import static java.nio.charset.StandardCharsets.UTF_8; + public class CoseEncryptionExample { private static final SecureRandom random = new SecureRandom(); @@ -36,6 +38,17 @@ public static void main(String... args) throws Exception { message.encrypt(); System.out.println(Base64url.encode(message.EncodeToBytes())); + // Print the CBOR structure of the message System.out.println(message.EncodeToCBORObject()); + + // To decrypt + var receivedMessage = (EncryptMessage) Message.DecodeFromBytes(message.EncodeToBytes()); + // The COSE reference implementation uses == to test for recipient equality + // (a bug?) so make sure to pass the exact same reference, but add back the key material + // that was stripped when generating the message. + var self = receivedMessage.getRecipient(0); + self.SetKey(new OneKey(keyData)); + receivedMessage.decrypt(self); + System.out.println("Received: " + new String(receivedMessage.GetContent(), UTF_8)); } } From 8646ac222c2aee2f147597f81dec006b2bfd9dfd Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 16 Mar 2020 10:26:40 +0000 Subject: [PATCH 198/209] Add NaCl CryptoBox example --- .../apisecurityinaction/NaclCborExample.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/NaclCborExample.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/NaclCborExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/NaclCborExample.java new file mode 100644 index 0000000..9f36bce --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/NaclCborExample.java @@ -0,0 +1,21 @@ +package com.manning.apisecurityinaction; + +import com.upokecenter.cbor.CBORObject; +import software.pando.crypto.nacl.*; + +public class NaclCborExample { + public static void main(String... args) { + var senderKeys = CryptoBox.keyPair(); + var recipientKeys = CryptoBox.keyPair(); + var cborMap = CBORObject.NewMap() + .Add("foo", "bar") + .Add("data", 12345); + var sent = CryptoBox.encrypt(senderKeys.getPrivate(), + recipientKeys.getPublic(), cborMap.EncodeToBytes()); + + var recvd = CryptoBox.fromString(sent.toString()); + var cbor = recvd.decrypt(recipientKeys.getPrivate(), + senderKeys.getPublic()); + System.out.println(CBORObject.DecodeFromBytes(cbor)); + } +} From 047c86f9142828287271472378329b241e3a5f94 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 23 Mar 2020 10:20:33 +0000 Subject: [PATCH 199/209] Add AES-SIV example --- natter-api/pom.xml | 5 +++ .../apisecurityinaction/AesSivExample.java | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/AesSivExample.java diff --git a/natter-api/pom.xml b/natter-api/pom.xml index 3d931be..0f64d35 100644 --- a/natter-api/pom.xml +++ b/natter-api/pom.xml @@ -80,6 +80,11 @@ cose-java 1.1.0 + + org.cryptomator + siv-mode + 1.3.2 + diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/AesSivExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/AesSivExample.java new file mode 100644 index 0000000..1f06680 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/AesSivExample.java @@ -0,0 +1,32 @@ +package com.manning.apisecurityinaction; + +import com.upokecenter.cbor.CBORObject; +import org.cryptomator.siv.SivMode; + +import javax.crypto.spec.SecretKeySpec; +import java.security.SecureRandom; +import java.util.Arrays; + +public class AesSivExample { + public static void main(String... args) throws Exception { + var psk = PskServer.loadPsk("changeit".toCharArray()); + var macKey = new SecretKeySpec(Arrays.copyOfRange(psk, 0, 16), + "AES"); + var encKey = new SecretKeySpec(Arrays.copyOfRange(psk, 16, 32), + "AES"); + + var randomIv = new byte[16]; + new SecureRandom().nextBytes(randomIv); + var header = "Test header".getBytes(); + var body = CBORObject.NewMap() + .Add("sensor", "F5671434") + .Add("reading", 1234).EncodeToBytes(); + + var siv = new SivMode(); + var ciphertext = siv.encrypt(encKey, macKey, body, + header, randomIv); + var plaintext = siv.decrypt(encKey, macKey, ciphertext, + header, randomIv); + assert Arrays.equals(plaintext, body); + } +} From bea6ad27b79c8a4e46bd41c7e56e31bbad4243eb Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Tue, 7 Apr 2020 11:00:36 +0100 Subject: [PATCH 200/209] Add example of ratcheting for forward secrecy --- .../apisecurityinaction/RatchetExample.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/RatchetExample.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/RatchetExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/RatchetExample.java new file mode 100644 index 0000000..618ff05 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/RatchetExample.java @@ -0,0 +1,30 @@ +package com.manning.apisecurityinaction; + +import org.bouncycastle.util.encoders.Hex; + +import javax.crypto.Cipher; +import javax.crypto.spec.*; +import java.util.Arrays; + +public class RatchetExample { + + public static void main(String... args) throws Exception { + var key = PskServer.loadPsk(args[0].toCharArray()); + System.out.println("Original key: " + Hex.toHexString(key)); + for (int i = 0; i < 10; ++i) { + var newKey = ratchet(key); + Arrays.fill(key, (byte) 0); + key = newKey; + System.out.println("Next key: " + Hex.toHexString(key)); + } + } + + private static byte[] ratchet(byte[] oldKey) throws Exception { + var cipher = Cipher.getInstance("AES/CTR/NoPadding"); + var iv = new byte[16]; + Arrays.fill(iv, (byte) 0xFF); + cipher.init(Cipher.ENCRYPT_MODE, + new SecretKeySpec(oldKey, "AES"), new IvParameterSpec(iv)); + return cipher.doFinal(new byte[32]); + } +} From e7b926a37b148a5893b890c38d5010a8d0accfbd Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 3 May 2020 13:46:08 +0100 Subject: [PATCH 201/209] Add device database --- .../apisecurityinaction/PskServer.java | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java index c08b90a..d88fa2e 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java @@ -1,17 +1,25 @@ package com.manning.apisecurityinaction; -import static java.nio.charset.StandardCharsets.UTF_8; +import org.bouncycastle.tls.*; +import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; +import org.dalesbred.Database; +import org.dalesbred.annotation.DalesbredInstantiator; +import org.h2.jdbcx.JdbcConnectionPool; +import software.pando.crypto.nacl.*; + import java.io.FileInputStream; import java.net.*; import java.security.*; -import org.bouncycastle.tls.*; -import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; + +import static java.nio.charset.StandardCharsets.UTF_8; public class PskServer { public static void main(String[] args) throws Exception { var psk = loadPsk(args[0].toCharArray()); + var encryptionKey = SecretBox.key(); + var deviceDb = createDatabase(encryptionKey, psk); var crypto = new BcTlsCrypto(new SecureRandom()); - var server = new PSKTlsServer(crypto, getIdentityManager(psk)) { + var server = new PSKTlsServer(crypto, getIdentityManager(deviceDb, encryptionKey)) { @Override protected ProtocolVersion[] getSupportedVersions() { return ProtocolVersion.DTLSv12.only(); @@ -47,7 +55,8 @@ protected int[] getSupportedCipherSuites() { } } - static TlsPSKIdentityManager getIdentityManager(byte[] psk) { + static TlsPSKIdentityManager getIdentityManager( + Database deviceDb, Key decryptionKey) { return new TlsPSKIdentityManager() { @Override public byte[] getHint() { @@ -56,7 +65,12 @@ public byte[] getHint() { @Override public byte[] getPSK(byte[] identity) { - return psk; + var device = deviceDb.findUnique(Device.class, + "SELECT device_id, psk_id, encrypted_psk " + + "FROM devices " + + "WHERE psk_id = ?", identity); + System.out.println("Loaded PSK from client device: " + device.deviceId); + return device.encryptedPsk.decrypt(decryptionKey); } }; } @@ -66,4 +80,33 @@ static byte[] loadPsk(char[] password) throws Exception { keyStore.load(new FileInputStream("keystore.p12"), password); return keyStore.getKey("aes-key", password).getEncoded(); } + + static Database createDatabase(Key encryptionKey, byte[] exampleDevicePsk) { + var pool = JdbcConnectionPool.create("jdbc:h2:mem:psk", "psk", "dummy"); + var database = Database.forDataSource(pool); + database.update("CREATE TABLE devices(" + + "device_id VARCHAR(255) PRIMARY KEY," + + "psk_id BINARY(64) NOT NULL," + + "encrypted_psk VARCHAR(1024) NOT NULL);"); + database.update("CREATE UNIQUE INDEX psk_id_idx ON devices(psk_id);"); + + var encryptedPsk = SecretBox.encrypt(encryptionKey, exampleDevicePsk).toString(); + database.update("INSERT INTO devices(device_id, psk_id, encrypted_psk) VALUES (?, ?, ?)", + "test", Crypto.hash(exampleDevicePsk), encryptedPsk); + + return database; + } + + public static class Device { + final String deviceId; + final byte[] pskId; + final SecretBox encryptedPsk; + + @DalesbredInstantiator + public Device(String deviceId, byte[] pskId, String encryptedPsk) { + this.deviceId = deviceId; + this.pskId = pskId; + this.encryptedPsk = SecretBox.fromString(encryptedPsk); + } + } } From ba517c6604b7af6e8438cd2f050bb2d99d789ec6 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Sun, 3 May 2020 13:50:06 +0100 Subject: [PATCH 202/209] Updated README for chapters 12 and 13 --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ea6f79e..c575149 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,10 @@ The API server for each chapter can be started using the command mvn clean compile exec:java This will start the Spark/Jetty server running on port 4567. See chapter -descriptions for HTTP requests that can be +descriptions for HTTP requests that can be used. + +Chapter 10 and onwards have more detailed requirements to run the sample code. +Please consult the book for exact instructions. ## Chapters @@ -89,3 +92,13 @@ descriptions for HTTP requests that can be - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter11) - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter11-end) + +### Chapter 12 - Securing IoT communications + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter12) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter12-end) + +### Chapter 13 - Securing IoT APIs + + - [Starting Point](https://github.com/NeilMadden/apisecurityinaction/tree/chapter13) + - [Finished Code](https://github.com/NeilMadden/apisecurityinaction/tree/chapter13-end) From f883df649e60508acaab56a9095380b295eba390 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 18 May 2020 14:59:52 +0100 Subject: [PATCH 203/209] Add DeviceIdentityManager and method to retrieve deviceId --- .../manning/apisecurityinaction/Device.java | 62 +++++++++++++++++ .../DeviceIdentityManager.java | 28 ++++++++ .../apisecurityinaction/PskClient.java | 10 ++- .../apisecurityinaction/PskServer.java | 67 ++++--------------- 4 files changed, 106 insertions(+), 61 deletions(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/Device.java create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DeviceIdentityManager.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Device.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Device.java new file mode 100644 index 0000000..6850f19 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Device.java @@ -0,0 +1,62 @@ +package com.manning.apisecurityinaction; + +import org.dalesbred.Database; +import org.dalesbred.annotation.DalesbredInstantiator; +import org.h2.jdbcx.JdbcConnectionPool; +import software.pando.crypto.nacl.SecretBox; + +import java.io.*; +import java.security.Key; +import java.util.Optional; + +public class Device { + final String deviceId; + final String manufacturer; + final String model; + final byte[] encryptedPsk; + + @DalesbredInstantiator + public Device(String deviceId, String manufacturer, + String model, byte[] encryptedPsk) { + this.deviceId = deviceId; + this.manufacturer = manufacturer; + this.model = model; + this.encryptedPsk = encryptedPsk; + } + + public byte[] getPsk(Key decryptionKey) { + try (var in = new ByteArrayInputStream(encryptedPsk)) { + var box = SecretBox.readFrom(in); + return box.decrypt(decryptionKey); + } catch (IOException e) { + throw new RuntimeException("Unable to decrypt PSK", e); + } + } + + static Database createDatabase(SecretBox encryptedPsk) throws IOException { + var pool = JdbcConnectionPool.create("jdbc:h2:mem:devices", + "devices", "password"); + var database = Database.forDataSource(pool); + + database.update("CREATE TABLE devices(" + + "device_id VARCHAR(30) PRIMARY KEY," + + "manufacturer VARCHAR(100) NOT NULL," + + "model VARCHAR(100) NOT NULL," + + "encrypted_psk VARBINARY(1024) NOT NULL)"); + + var out = new ByteArrayOutputStream(); + encryptedPsk.writeTo(out); + database.update("INSERT INTO devices(" + + "device_id, manufacturer, model, encrypted_psk) " + + "VALUES(?, ?, ?, ?)", "test", "example", "ex001", + out.toByteArray()); + + return database; + } + + static Optional find(Database database, String deviceId) { + return database.findOptional(Device.class, + "SELECT device_id, manufacturer, model, encrypted_psk " + + "FROM devices WHERE device_id = ?", deviceId); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceIdentityManager.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceIdentityManager.java new file mode 100644 index 0000000..65579f9 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceIdentityManager.java @@ -0,0 +1,28 @@ +package com.manning.apisecurityinaction; +import org.bouncycastle.tls.TlsPSKIdentityManager; +import org.dalesbred.Database; +import java.security.Key; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class DeviceIdentityManager implements TlsPSKIdentityManager { + private final Database database; + private final Key pskDecryptionKey; + + public DeviceIdentityManager(Database database, Key pskDecryptionKey) { + this.database = database; + this.pskDecryptionKey = pskDecryptionKey; + } + + @Override + public byte[] getHint() { + return null; + } + + @Override + public byte[] getPSK(byte[] identity) { + var deviceId = new String(identity, UTF_8); + return Device.find(database, deviceId) + .map(device -> device.getPsk(pskDecryptionKey)) + .orElse(null); + } +} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java index 05d4a53..99fb742 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java @@ -1,20 +1,18 @@ package com.manning.apisecurityinaction; -import static java.nio.charset.StandardCharsets.UTF_8; +import org.bouncycastle.tls.*; +import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; import java.net.*; import java.nio.file.*; import java.security.SecureRandom; -import org.bouncycastle.tls.*; -import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; - -import software.pando.crypto.nacl.Crypto; +import static java.nio.charset.StandardCharsets.UTF_8; public class PskClient { public static void main(String[] args) throws Exception { var psk = PskServer.loadPsk(args[0].toCharArray()); - var pskId = Crypto.hash(psk); + var pskId = "test".getBytes(UTF_8); var crypto = new BcTlsCrypto(new SecureRandom()); var client = new PSKTlsClient(crypto, pskId, psk) { diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java index d88fa2e..7721da2 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java @@ -2,10 +2,7 @@ import org.bouncycastle.tls.*; import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; -import org.dalesbred.Database; -import org.dalesbred.annotation.DalesbredInstantiator; -import org.h2.jdbcx.JdbcConnectionPool; -import software.pando.crypto.nacl.*; +import software.pando.crypto.nacl.SecretBox; import java.io.FileInputStream; import java.net.*; @@ -17,9 +14,11 @@ public class PskServer { public static void main(String[] args) throws Exception { var psk = loadPsk(args[0].toCharArray()); var encryptionKey = SecretBox.key(); - var deviceDb = createDatabase(encryptionKey, psk); + var deviceDb = Device.createDatabase( + SecretBox.encrypt(encryptionKey, psk)); var crypto = new BcTlsCrypto(new SecureRandom()); - var server = new PSKTlsServer(crypto, getIdentityManager(deviceDb, encryptionKey)) { + var server = new PSKTlsServer(crypto, + new DeviceIdentityManager(deviceDb, encryptionKey)) { @Override protected ProtocolVersion[] getSupportedVersions() { return ProtocolVersion.DTLSv12.only(); @@ -36,6 +35,11 @@ protected int[] getSupportedCipherSuites() { CipherSuite.TLS_PSK_WITH_CHACHA20_POLY1305_SHA256 }; } + + String getPeerDeviceIdentity() { + return new String(context.getSecurityParametersConnection() + .getPSKIdentity(), UTF_8); + } }; var buffer = new byte[2048]; var serverSocket = new DatagramSocket(54321); @@ -50,63 +54,16 @@ protected int[] getSupportedCipherSuites() { while (true) { var len = dtls.receive(buffer, 0, buffer.length, 60000); if (len == -1) break; + System.out.println("Receiving data from device: " + + server.getPeerDeviceIdentity()); var data = new String(buffer, 0, len, UTF_8); System.out.println("Received: " + data); } } - static TlsPSKIdentityManager getIdentityManager( - Database deviceDb, Key decryptionKey) { - return new TlsPSKIdentityManager() { - @Override - public byte[] getHint() { - return null; - } - - @Override - public byte[] getPSK(byte[] identity) { - var device = deviceDb.findUnique(Device.class, - "SELECT device_id, psk_id, encrypted_psk " + - "FROM devices " + - "WHERE psk_id = ?", identity); - System.out.println("Loaded PSK from client device: " + device.deviceId); - return device.encryptedPsk.decrypt(decryptionKey); - } - }; - } - static byte[] loadPsk(char[] password) throws Exception { var keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new FileInputStream("keystore.p12"), password); return keyStore.getKey("aes-key", password).getEncoded(); } - - static Database createDatabase(Key encryptionKey, byte[] exampleDevicePsk) { - var pool = JdbcConnectionPool.create("jdbc:h2:mem:psk", "psk", "dummy"); - var database = Database.forDataSource(pool); - database.update("CREATE TABLE devices(" + - "device_id VARCHAR(255) PRIMARY KEY," + - "psk_id BINARY(64) NOT NULL," + - "encrypted_psk VARCHAR(1024) NOT NULL);"); - database.update("CREATE UNIQUE INDEX psk_id_idx ON devices(psk_id);"); - - var encryptedPsk = SecretBox.encrypt(encryptionKey, exampleDevicePsk).toString(); - database.update("INSERT INTO devices(device_id, psk_id, encrypted_psk) VALUES (?, ?, ?)", - "test", Crypto.hash(exampleDevicePsk), encryptedPsk); - - return database; - } - - public static class Device { - final String deviceId; - final byte[] pskId; - final SecretBox encryptedPsk; - - @DalesbredInstantiator - public Device(String deviceId, byte[] pskId, String encryptedPsk) { - this.deviceId = deviceId; - this.pskId = pskId; - this.encryptedPsk = SecretBox.fromString(encryptedPsk); - } - } } From fa4704a4c0b15202f5f48acd9d257a127a4caf60 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 18 May 2020 15:50:01 +0100 Subject: [PATCH 204/209] Add example of replay protection using ETags --- .../ReplayProtectionExample.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java new file mode 100644 index 0000000..a70259d --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java @@ -0,0 +1,89 @@ +package com.manning.apisecurityinaction; + +import com.upokecenter.cbor.CBORObject; +import software.pando.crypto.nacl.CryptoBox; + +import java.net.URI; +import java.net.http.*; +import java.security.KeyPair; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.net.http.HttpResponse.BodyHandlers.ofString; +import static spark.Spark.*; + +public class ReplayProtectionExample implements Runnable { + private static final KeyPair clientKeys = CryptoBox.keyPair(); + private static final KeyPair serverKeys = CryptoBox.keyPair(); + + public static void main(String... args) throws Exception { + new Thread(new ReplayProtectionExample()).start(); + + var revisionEtag = "42"; + var headers = CBORObject.NewMap() + .Add("If-Matches", revisionEtag); + var body = CBORObject.NewMap() + .Add("foo", "bar") + .Add("data", 12345); + var request = CBORObject.NewMap() + .Add("method", "PUT") + .Add("headers", headers) + .Add("body", body); + var sent = CryptoBox.encrypt(clientKeys.getPrivate(), + serverKeys.getPublic(), request.EncodeToBytes()); + + var httpRequest = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:4567/test")) + .header("If-Matches", revisionEtag) + .PUT(HttpRequest.BodyPublishers.ofString(sent.toString())) + .build(); + var httpResponse = HttpClient.newHttpClient().send(httpRequest, ofString()); + System.out.println("Received response: " + httpResponse.statusCode()); + System.out.println("ETag: " + httpResponse.headers().allValues("ETag")); + } + + @Override + public void run() { + + before((request, response) -> { + var encryptedRequest = CryptoBox.fromString(request.body()); + var decrypted = encryptedRequest.decrypt( + serverKeys.getPrivate(), clientKeys.getPublic()); + var cbor = CBORObject.DecodeFromBytes(decrypted); + + if (!cbor.get("method").AsString().equals(request.requestMethod())) { + halt(403); + } + + var expectedHeaders = cbor.get("headers"); + for (var headerName : expectedHeaders.getKeys()) { + if (!expectedHeaders.get(headerName).AsString() + .equals(request.headers(headerName.AsString()))) { + halt(403); + } + } + + request.attribute("decryptedRequest", cbor.get("body")); + }); + + // Simulate updating an ETag using an AtomicInteger. In a + // real example the ETag would be stored alongside the data + // and updated in a transaction. + var etag = new AtomicInteger(42); + put("/test", (request, response) -> { + CBORObject decryptedRequest = request.attribute("decryptedRequest"); + var expectedEtag = Integer.parseInt(request.headers("If-Matches")); + + if (!etag.compareAndSet(expectedEtag, expectedEtag + 1)) { + response.status(412); + return null; + } + + System.out.println("Updating resource with new content: " + decryptedRequest); + + response.status(200); + response.header("ETag", String.valueOf(expectedEtag + 1)); + response.type("text/plain"); + return "OK"; + }); + } +} From c70aa0b0ecea7465ffd6564e575eb4b6f2e7f52e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Mon, 18 May 2020 16:26:42 +0100 Subject: [PATCH 205/209] Tweaks for readability. --- .../apisecurityinaction/ReplayProtectionExample.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java index a70259d..097c96f 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java @@ -8,6 +8,7 @@ import java.security.KeyPair; import java.util.concurrent.atomic.AtomicInteger; +import static java.lang.Integer.parseInt; import static java.net.http.HttpResponse.BodyHandlers.ofString; import static spark.Spark.*; @@ -50,7 +51,8 @@ public void run() { serverKeys.getPrivate(), clientKeys.getPublic()); var cbor = CBORObject.DecodeFromBytes(decrypted); - if (!cbor.get("method").AsString().equals(request.requestMethod())) { + if (!cbor.get("method").AsString() + .equals(request.requestMethod())) { halt(403); } @@ -70,15 +72,15 @@ public void run() { // and updated in a transaction. var etag = new AtomicInteger(42); put("/test", (request, response) -> { - CBORObject decryptedRequest = request.attribute("decryptedRequest"); - var expectedEtag = Integer.parseInt(request.headers("If-Matches")); + var expectedEtag = parseInt(request.headers("If-Matches")); if (!etag.compareAndSet(expectedEtag, expectedEtag + 1)) { response.status(412); return null; } - System.out.println("Updating resource with new content: " + decryptedRequest); + System.out.println("Updating resource with new content: " + + request.attribute("decryptedRequest")); response.status(200); response.header("ETag", String.valueOf(expectedEtag + 1)); From 751d611e03ecdeba609122665cb7eb7aa19f5d9c Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Thu, 21 May 2020 11:57:30 +0100 Subject: [PATCH 206/209] Add OAuth2 device authorization grant example --- .../DeviceGrantClient.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DeviceGrantClient.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceGrantClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceGrantClient.java new file mode 100644 index 0000000..44f5566 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceGrantClient.java @@ -0,0 +1,85 @@ +package com.manning.apisecurityinaction; + +import org.json.JSONObject; +import java.net.*; +import java.net.http.*; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; +import java.util.concurrent.TimeUnit; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class DeviceGrantClient { + private static final HttpClient httpClient = HttpClient.newHttpClient(); + + public static void main(String... args) throws Exception { + var clientId = "deviceGrantTest"; + var scope = "a b c"; + + // Make initial request to device authorization endpoint + var json = beginDeviceAuthorization(clientId, scope); + var deviceCode = json.getString("device_code"); + var interval = json.optInt("interval", 5); + System.out.println("Please open " + json.getString("verification_uri")); + System.out.println("And enter code:\n\t" + json.getString("user_code")); + System.out.println("I'm waiting!"); + + while (true) { + Thread.sleep(TimeUnit.SECONDS.toMillis(interval)); + json = pollAccessTokenEndpoint(clientId, deviceCode); + var error = json.optString("error", null); + if (error != null) { + switch (error) { + case "slow_down": + System.out.println("Slowing down"); + interval += 5; + break; + case "authorization_pending": + System.out.println("Still waiting!"); + break; + default: + System.err.println("Authorization failed: " + error); + System.exit(1); + break; + } + } else { + System.out.println("Access token: " + json.getString("access_token")); + break; + } + } + } + + private static JSONObject beginDeviceAuthorization( + String clientId, String scope) throws Exception { + var form = "client_id=" + URLEncoder.encode(clientId, UTF_8) + + "&scope=" + URLEncoder.encode(scope, UTF_8) + + "&response_type=device_code"; + var request = HttpRequest.newBuilder() + .header("Content-Type", + "application/x-www-form-urlencoded") + .uri(URI.create( + "https://as.example.com:8443/openam/oauth2/device/code")) + .POST(BodyPublishers.ofString(form)) + .build(); + var response = httpClient.send(request, BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Bad response from AS: " + response.body()); + } + return new JSONObject(response.body()); + } + + private static JSONObject pollAccessTokenEndpoint( + String clientId, String deviceCode) throws Exception { + var form = "client_id=" + clientId + + "&grant_type=urn:ietf:params:oauth:grant-type:device_code" + + "&device_code=" + URLEncoder.encode(deviceCode, UTF_8); + + var request = HttpRequest.newBuilder() + .header("Content-Type", "application/x-www-form-urlencoded") + .uri(URI.create("https://as.example.com:8443/openam/oauth2/access_token")) + .POST(BodyPublishers.ofString(form)) + .build(); + var response = httpClient.send(request, BodyHandlers.ofString()); + return new JSONObject(response.body()); + } +} From 3c61b6c1948f1ee528e2c7b50cc8820aab73463e Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 22 May 2020 16:12:29 +0100 Subject: [PATCH 207/209] Add OSCORE implementation --- .../com/manning/apisecurityinaction/HKDF.java | 21 +++- .../manning/apisecurityinaction/Oscore.java | 116 ++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/Oscore.java diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java b/natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java index 7823476..8c3c87d 100644 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java @@ -8,9 +8,28 @@ import static java.util.Objects.checkIndex; public class HKDF { + public static Key extract(byte[] salt, byte[] inputKeyMaterial) + throws GeneralSecurityException { + var hmac = Mac.getInstance("HmacSHA256"); + if (salt == null) { + salt = new byte[hmac.getMacLength()]; + } + hmac.init(new SecretKeySpec(salt, "HmacSHA256")); + return new SecretKeySpec(hmac.doFinal(inputKeyMaterial), + "HmacSHA256"); + } + public static Key expand(Key masterKey, String context, int outputKeySize, String algorithm) throws GeneralSecurityException { + return expand(masterKey, context.getBytes(UTF_8), + outputKeySize, algorithm); + } + + public static Key expand(Key masterKey, byte[] context, + int outputKeySize, String algorithm) + throws GeneralSecurityException { + checkIndex(outputKeySize, 255*32); var hmac = Mac.getInstance("HmacSHA256"); @@ -20,7 +39,7 @@ public static Key expand(Key masterKey, String context, var block = new byte[0]; for (int i = 0; i < outputKeySize; i += 32) { hmac.update(block); - hmac.update(context.getBytes(UTF_8)); + hmac.update(context); hmac.update((byte) ((i / 32) + 1)); block = hmac.doFinal(); System.arraycopy(block, 0, output, i, diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Oscore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Oscore.java new file mode 100644 index 0000000..d27a116 --- /dev/null +++ b/natter-api/src/main/java/com/manning/apisecurityinaction/Oscore.java @@ -0,0 +1,116 @@ +package com.manning.apisecurityinaction; + +import COSE.*; +import com.upokecenter.cbor.CBORObject; +import org.apache.commons.codec.binary.Hex; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.nio.*; +import java.security.*; + +public class Oscore { + + private static Key deriveKey(Key hkdfKey, byte[] id, + byte[] idContext, AlgorithmID coseAlgorithm) + throws GeneralSecurityException { + + int keySizeBytes = coseAlgorithm.getKeySize() / 8; + CBORObject context = CBORObject.NewArray(); + context.Add(id); + context.Add(idContext); + context.Add(coseAlgorithm.AsCBOR()); + context.Add(CBORObject.FromObject("Key")); + context.Add(keySizeBytes); + + return HKDF.expand(hkdfKey, context.EncodeToBytes(), + keySizeBytes, "AES"); + } + + private static byte[] deriveCommonIV(Key hkdfKey, + byte[] idContext, AlgorithmID coseAlgorithm, int ivLength) + throws GeneralSecurityException { + CBORObject context = CBORObject.NewArray(); + context.Add(new byte[0]); + context.Add(idContext); + context.Add(coseAlgorithm.AsCBOR()); + context.Add(CBORObject.FromObject("IV")); + context.Add(ivLength); + + return HKDF.expand(hkdfKey, context.EncodeToBytes(), + ivLength, "dummy").getEncoded(); + } + + private static byte[] nonce(int ivLength, long sequenceNumber, + byte[] id, byte[] commonIv) { + if (sequenceNumber > (1L << 40)) + throw new IllegalArgumentException("Sequence number too large"); + int idLen = ivLength - 6; + if (id.length > idLen) + throw new IllegalArgumentException("ID is too large"); + + var buffer = ByteBuffer.allocate(ivLength).order(ByteOrder.BIG_ENDIAN); + buffer.put((byte) id.length); + buffer.put(new byte[idLen - id.length]); + buffer.put(id); + buffer.put((byte) ((sequenceNumber >>> 32) & 0xFF)); + buffer.putInt((int) sequenceNumber); + return xor(buffer.array(), commonIv); + } + + private static byte[] xor(byte[] xs, byte[] ys) { + for (int i = 0; i < xs.length; ++i) + xs[i] ^= ys[i]; + return xs; + } + + public static void main(String... args) throws Exception { + var algorithm = AlgorithmID.AES_CCM_16_64_128; + var masterKey = new byte[] { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 + }; + var masterSalt = new byte[] { + (byte) 0x9e, 0x7c, (byte) 0xa9, 0x22, 0x23, 0x78, + 0x63, 0x40 + }; + var hkdfKey = HKDF.extract(masterSalt, masterKey); + var senderId = new byte[0]; + var recipientId = new byte[] { 0x01 }; + + var senderKey = deriveKey(hkdfKey, senderId, null, algorithm); + var recipientKey = deriveKey(hkdfKey, recipientId, null, algorithm); + var commonIv = deriveCommonIV(hkdfKey, null, algorithm, 13); + + System.out.println(Hex.encodeHex(senderKey.getEncoded())); + System.out.println(Hex.encodeHex(recipientKey.getEncoded())); + System.out.println(Hex.encodeHex(commonIv)); + + long sequenceNumber = 20L; + byte[] nonce = nonce(13, sequenceNumber, senderId, commonIv); + byte[] partialIv = new byte[] { (byte) sequenceNumber }; + + var message = new Encrypt0Message(); + message.addAttribute(HeaderKeys.Algorithm, + algorithm.AsCBOR(), Attribute.DO_NOT_SEND); + message.addAttribute(HeaderKeys.IV, + nonce, Attribute.DO_NOT_SEND); + message.addAttribute(HeaderKeys.PARTIAL_IV, + partialIv, Attribute.UNPROTECTED); + message.addAttribute(HeaderKeys.KID, + senderId, Attribute.UNPROTECTED); + message.SetContent( + new byte[] { 0x01, (byte) 0xb3, 0x74, 0x76, 0x31}); + + var associatedData = CBORObject.NewArray(); + associatedData.Add(1); + associatedData.Add(algorithm.AsCBOR()); + associatedData.Add(senderId); + associatedData.Add(partialIv); + associatedData.Add(new byte[0]); + message.setExternal(associatedData.EncodeToBytes()); + + Security.addProvider(new BouncyCastleProvider()); + message.encrypt(senderKey.getEncoded()); + System.out.println(Hex.encodeHex(message.getEncryptedContent())); + } +} From 062baef9f6f808ce9bce80b37df7384c2dcf061b Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Wed, 5 Aug 2020 14:24:44 +0100 Subject: [PATCH 208/209] Remove code from master branch (see per-chapter branches) --- README.md | 7 +- natter-api/api.natter.local-key.pem | 28 -- natter-api/api.natter.local.pem | 26 -- natter-api/as.example.com.ca.p12 | Bin 1506 -> 0 bytes natter-api/as.example.com.p12 | Bin 1506 -> 0 bytes natter-api/docker/h2/Dockerfile | 22 -- natter-api/keystore.p12 | Bin 2145 -> 0 bytes .../kubernetes/natter-api-deployment.yaml | 37 --- natter-api/kubernetes/natter-api-service.yaml | 13 - .../natter-database-deployment.yaml | 29 -- .../kubernetes/natter-database-service.yaml | 11 - natter-api/kubernetes/natter-ingress.yaml | 25 -- .../natter-link-preview-deployment.yaml | 29 -- .../natter-link-preview-service.yaml | 11 - natter-api/kubernetes/natter-namespace.yaml | 8 - natter-api/localhost.p12 | Bin 3975 -> 0 bytes natter-api/natter-api-service.p12 | Bin 4015 -> 0 bytes natter-api/natter-database-service.p12 | Bin 4023 -> 0 bytes natter-api/natter-token-database-service.p12 | Bin 4031 -> 0 bytes natter-api/natter-token-service.p12 | Bin 4015 -> 0 bytes natter-api/pom.xml | 140 ---------- natter-api/server.pem | 26 -- .../apisecurityinaction/AesSivExample.java | 32 --- .../apisecurityinaction/CorsFilter.java | 41 --- .../CoseEncryptionExample.java | 54 ---- .../manning/apisecurityinaction/Device.java | 62 ----- .../DeviceGrantClient.java | 85 ------ .../DeviceIdentityManager.java | 28 -- .../apisecurityinaction/DtlsClient.java | 61 ----- .../DtlsDatagramChannel.java | 248 ------------------ .../apisecurityinaction/DtlsServer.java | 46 ---- .../com/manning/apisecurityinaction/HKDF.java | 51 ---- .../apisecurityinaction/JwtBearerClient.java | 64 ----- .../apisecurityinaction/LinkPreviewer.java | 109 -------- .../com/manning/apisecurityinaction/Main.java | 203 -------------- .../apisecurityinaction/NaclCborExample.java | 21 -- .../manning/apisecurityinaction/Oscore.java | 116 -------- .../apisecurityinaction/PskClient.java | 49 ---- .../apisecurityinaction/PskServer.java | 69 ----- .../apisecurityinaction/RatchetExample.java | 30 --- .../ReplayProtectionExample.java | 91 ------- .../RevokeAccessToken.java | 46 ---- .../apisecurityinaction/TokenService.java | 59 ----- .../controller/ABACAccessController.java | 57 ---- .../controller/AuditController.java | 89 ------- .../AuthorizationServerController.java | 119 --------- .../controller/CapabilityController.java | 46 ---- .../controller/DroolsAccessController.java | 55 ---- .../controller/IdTokenValidationFilter.java | 72 ----- .../controller/LdapUserController.java | 90 ------- .../controller/ModeratorController.java | 25 -- .../controller/SpaceController.java | 225 ---------------- .../controller/TokenController.java | 92 ------- .../controller/UserController.java | 174 ------------ .../token/AuthenticatedTokenStore.java | 4 - .../apisecurityinaction/token/Base64url.java | 18 -- .../token/ConfidentialTokenStore.java | 4 - .../token/CookieTokenStore.java | 73 ------ .../token/DatabaseTokenStore.java | 86 ------ .../token/EncryptedJwtTokenStore.java | 96 ------- .../token/EncryptedTokenStore.java | 38 --- .../token/HmacTokenStore.java | 80 ------ .../token/JsonTokenStore.java | 55 ---- .../token/JwtHeaderTokenStore.java | 57 ---- .../token/MacaroonTokenStore.java | 78 ------ .../token/OAuth2TokenStore.java | 167 ------------ .../token/RemoteTokenStore.java | 76 ------ .../token/SecureTokenStore.java | 5 - .../token/SignedJwtAccessTokenStore.java | 76 ------ .../token/SignedJwtTokenStore.java | 73 ------ .../apisecurityinaction/token/TokenStore.java | 47 ---- .../token/UnauthenticatedEncryptionStore.java | 72 ----- natter-api/src/main/jib/keystore.p12 | Bin 755 -> 0 bytes natter-api/src/main/jib/localhost.p12 | Bin 3975 -> 0 bytes .../src/main/resources/META-INF/kmodule.xml | 3 - natter-api/src/main/resources/accessrules.drl | 15 -- .../src/main/resources/public/capability.html | 17 -- .../src/main/resources/public/capability.js | 36 --- .../src/main/resources/public/login.html | 22 -- natter-api/src/main/resources/public/login.js | 37 --- .../src/main/resources/public/natter.html | 21 -- .../src/main/resources/public/natter.js | 42 --- natter-api/src/main/resources/schema.sql | 71 ----- natter-api/test.txt | 26 -- natter-api/xss.html | 13 - 85 files changed, 5 insertions(+), 4424 deletions(-) delete mode 100644 natter-api/api.natter.local-key.pem delete mode 100644 natter-api/api.natter.local.pem delete mode 100644 natter-api/as.example.com.ca.p12 delete mode 100644 natter-api/as.example.com.p12 delete mode 100644 natter-api/docker/h2/Dockerfile delete mode 100644 natter-api/keystore.p12 delete mode 100644 natter-api/kubernetes/natter-api-deployment.yaml delete mode 100644 natter-api/kubernetes/natter-api-service.yaml delete mode 100644 natter-api/kubernetes/natter-database-deployment.yaml delete mode 100644 natter-api/kubernetes/natter-database-service.yaml delete mode 100644 natter-api/kubernetes/natter-ingress.yaml delete mode 100644 natter-api/kubernetes/natter-link-preview-deployment.yaml delete mode 100644 natter-api/kubernetes/natter-link-preview-service.yaml delete mode 100644 natter-api/kubernetes/natter-namespace.yaml delete mode 100644 natter-api/localhost.p12 delete mode 100644 natter-api/natter-api-service.p12 delete mode 100644 natter-api/natter-database-service.p12 delete mode 100644 natter-api/natter-token-database-service.p12 delete mode 100644 natter-api/natter-token-service.p12 delete mode 100644 natter-api/pom.xml delete mode 100644 natter-api/server.pem delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/AesSivExample.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/Device.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DeviceGrantClient.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DeviceIdentityManager.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/JwtBearerClient.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewer.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/Main.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/NaclCborExample.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/Oscore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/RatchetExample.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuditController.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuthorizationServerController.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/DroolsAccessController.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/IdTokenValidationFilter.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/LdapUserController.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/ModeratorController.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/AuthenticatedTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/Base64url.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/ConfidentialTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedJwtTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/MacaroonTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/RemoteTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/SecureTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java delete mode 100644 natter-api/src/main/java/com/manning/apisecurityinaction/token/UnauthenticatedEncryptionStore.java delete mode 100644 natter-api/src/main/jib/keystore.p12 delete mode 100644 natter-api/src/main/jib/localhost.p12 delete mode 100644 natter-api/src/main/resources/META-INF/kmodule.xml delete mode 100644 natter-api/src/main/resources/accessrules.drl delete mode 100644 natter-api/src/main/resources/public/capability.html delete mode 100644 natter-api/src/main/resources/public/capability.js delete mode 100644 natter-api/src/main/resources/public/login.html delete mode 100644 natter-api/src/main/resources/public/login.js delete mode 100644 natter-api/src/main/resources/public/natter.html delete mode 100644 natter-api/src/main/resources/public/natter.js delete mode 100644 natter-api/src/main/resources/schema.sql delete mode 100644 natter-api/test.txt delete mode 100644 natter-api/xss.html diff --git a/README.md b/README.md index c575149..5fe12f7 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,16 @@ This repository contains source code to accompany the upcoming book API Security in Action, written by Neil Madden and to be published by -Manning Publications some time next year. If you have stumbled across +Manning Publications in October 2020. If you have stumbled across this repository by accident, it is unlikely to make much sense on its own at this stage. Please see [Manning's website](https://www.manning.com/books/api-security-in-action?a_aid=api_security_in_action&a_bid=6806e3b6) for early access. +**Note: there is no source code on the main branch.** You need to check +out the branch for the chapter you are reading. + The git repo is organized with a separate branch for each chapter, -starting with Chapter 2. Actually there are two (or more) branches +starting with Chapter 2. Actually there are two branches per chapter. The branches called "chapter02", "chapter03" etc will give you the source code as needed for starting out on the given chapter. The branches named "chapter02-end", "chapter03-end" etc give the diff --git a/natter-api/api.natter.local-key.pem b/natter-api/api.natter.local-key.pem deleted file mode 100644 index 912de37..0000000 --- a/natter-api/api.natter.local-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC9SSZNB9OPTnCa -2/w/jnbm9MERUkuTSeCRW+s2YbHu5P9s1sizyZ54qaFpqmzUiYc+u9PuhBNctfP2 -N4FdOnNjcZCh7xaHTiUIm4DERLkJOvokTjTQv4myZ4N9ePZeKIE41CKKUeYF/llQ -gCtR4E9BYFzkRyDBy/FVYweunCXKuG0zvfvOhTTtvOCgBZEjRVdO/53qOEcn5ELn -NjEpG2QpepXDuTwCmruUkVNqPppEckyknRxV4/LVaS3zTMEwrnDH172ky3GGOiZD -3zU5gUqNM7DaIZd2WuqGJk9HW2kNhG8U9Htaiijeb/PlDAMmAbeTWguvWzbwPCo3 -74W5TdhdAgMBAAECggEBAK9FpeacQaUoQBLVct0zQRyZNJGif4KyXPSchc/EZOvO -NkqFFDGOl2QpxuI+QioH8yj+6b6po/gsL+wk92/paOGDTib0agr+LEKtI24aKLDI -YMnvdO56/bkqKtKriI4luYpyvE0SiwmvvOpS1Eorh5gE798dkdKB00V6vqlLw57S -z9B5y9jpsg+CRMQuxfFL24CJXlnFFg9WpD8GNTlKTQkp/c88VRmZXmcc4sioPdV3 -GqekRrqTlpcRdvws1aOPN7DiKY9LSdzlaaUOH30T2iJ28DPwe5Zg44tuFuvwn3Wi -ssA2ZlwDdvPhXEI9kwH7cQzelmwCIGboSZHtUkaPAw0CgYEA8/Kh56OAgFfpVr1k -0WrGWm2GvYxQslXtrspG6l4648hOzDrKUh69nPH/r4nmzy+ODKcKVj3ckeAioXEb -Ss/a4mv+KOlulJK7nt+/thz338qIdBZmJDrDT9YLo7GtKUb5tbxgzKGd9Cgi2SGA -Nd+KiYgobJEPddOmuQyxgEFIwQMCgYEAxqMrpMm+7EotvvgSkWA6mBLc7EiUBq8s -qEdlb5IX+kizCoQLHOizTTa3pjlIFfHG6w5jeoQHl9rfKVh+eep36HLiMpC6GY7B -U52L2uAytGsdDuoGYZdxpX6PM2UnJDCNYUoPdIwjx9f8EQQ95uIqGnkHQycEePd1 -P6a0qn0j0x8CgYBADKlzvxsDF5HdQ1bQIR+5KF6jL88UM7l3FgbujBUcL0B5IMp0 -KzwPk/5U4XknVs4OBmGRaSabamTNTHwk9VP79OzDYx60hZ4bRZX5Q7vVF0EicasZ -wg/7yzA9J25WkxsHG1GzCJAHRe54YfJesrWWDJjIgIG1pv90QJ/uE7X9bwKBgQC8 -wa+mf0Qbe/3unAPg+6WSf1JKgkmP9ISmQHpGxHhekRj6JDH/Pa2s8RMhNQuoNsHE -+j5T3QTuK8Gmo35EUiexzwHd9SOzR7G0yGBvFF96jNLnKkH4GRaYoiRoPXYtcKnY -yqzXHpidvkO80+AS99X0pA/fo0MfxF85pivGWvZhFwKBgQDzl159KnPOm+bNgQtj -ofl3LIErQLorY3BzvSESo8YJ75RbOQfKqCS/QS7M5eI35HKPz7mG/w/e1aW5+N+D -bheLxNWKdV3suyV/fdkgVMXnzu/qEf62uDuOGqYtin8sMITc4fda+Su2cC2MwiLN -84KjzkwozDSLAyEa9tWKqY/4rA== ------END PRIVATE KEY----- diff --git a/natter-api/api.natter.local.pem b/natter-api/api.natter.local.pem deleted file mode 100644 index 4dd6bcb..0000000 --- a/natter-api/api.natter.local.pem +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEVzCCAr+gAwIBAgIRAIg4YhERsteD+T8/o4gLbGkwDQYJKoZIhvcNAQELBQAw -eTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMScwJQYDVQQLDB5uZWls -QGd1ZXN0MnMtTWFjQm9vay1Qcm8ubG9jYWwxLjAsBgNVBAMMJW1rY2VydCBuZWls -QGd1ZXN0MnMtTWFjQm9vay1Qcm8ubG9jYWwwHhcNMTkwNjAxMDAwMDAwWhcNMjkx -MTI0MTQ1ODQwWjBgMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlm -aWNhdGUxNTAzBgNVBAsMLG5laWxAZ3Vlc3Qycy1NYWNCb29rLVByby5sb2NhbCAo -TmVpbCBNYWRkZW4pMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvUkm -TQfTj05wmtv8P4525vTBEVJLk0ngkVvrNmGx7uT/bNbIs8meeKmhaaps1ImHPrvT -7oQTXLXz9jeBXTpzY3GQoe8Wh04lCJuAxES5CTr6JE400L+JsmeDfXj2XiiBONQi -ilHmBf5ZUIArUeBPQWBc5EcgwcvxVWMHrpwlyrhtM737zoU07bzgoAWRI0VXTv+d -6jhHJ+RC5zYxKRtkKXqVw7k8Apq7lJFTaj6aRHJMpJ0cVePy1Wkt80zBMK5wx9e9 -pMtxhjomQ981OYFKjTOw2iGXdlrqhiZPR1tpDYRvFPR7Wooo3m/z5QwDJgG3k1oL -r1s28DwqN++FuU3YXQIDAQABo3MwcTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAww -CgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBTXxvVMeFvupdtt -nvRQq/5lEN7kLjAbBgNVHREEFDASghBhcGkubmF0dGVyLmxvY2FsMA0GCSqGSIb3 -DQEBCwUAA4IBgQBfM8/BqK/3Lk3cNX4CJFVgy6v07zS2QPLUpPzisJ/DTMa1NPgB -My6oSUM52DNH87hjtkxR33j4rBriOQ3lOnDpbUahYHLNDyvfx4Waq0+kkkk5IXbI -eXrPN1kjUM55xOx1lh95N0elUn2zsofnMBqUbGcCk2q36EgA9yXLRYjlfq9fV+vY -1NvvMF7jjQRPgQ8+5N+dmOudWnDR+ADY0epSYUqQtHqNVDlbR8wS3q8Ws8gvweUD -4QhOpVkDLpdoqHxRuPW8U/EkcH8NuskTeTP9CmoQfzqmUAG1BqM6umm4r8uhRabh -7SpYuwU94EmoHK8+VLAAIM9VmNHRfqkKgOiQhk8/Ayv9csR9KrIt/i+yKCDERy5l -9U0yyXMI7FD1zbraBLhNweWlXdTcQKim2BXoYqg7wjgay770m/9ktD+aYyoPCmI6 -0dS/dMsWaD6S7im1jgOMtEr7hnFr7xRopTUett57Zn8mw477IhJgRzI85ZkoIhnJ -ZTYUGqjkdP1N/s8= ------END CERTIFICATE----- diff --git a/natter-api/as.example.com.ca.p12 b/natter-api/as.example.com.ca.p12 deleted file mode 100644 index d8c20d2015c27d90034fbad2f8bdba54b6df148a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1506 zcmV<81s(b@f(70J0Ru3C1(ya1Duzgg_YDCD0ic2fhy;QKgfM~yfG~mudf@^5iTk2T`FeGD9|F7bHqVn8VbOMrAFjYo`km8( zJvb8=$W;W2RaOeG%g4{aq3`7`1r8pF{L3yBFRuK>y=lsiP!Mho(US0q3k2vcI|s=`|{e zX7(0?lpt!9ruO|}IFBam8!C4zT}I;w(k0u6U-He_=%@ttFi%Z0^GN6o(gX5FzM zcCNDU8>bs|nDRZd?Mxcqs;Vb}LCQKas<;T__*=h)_>LdGSh~wBcCL`;xTd@VcRbb} zgA8nUE2ETBz8!}J|F%{fDm^Vg$~qhQX6912@lcWXUD)N5nbOSeRfQuo-q^v;BtRSS zKPSNv3n%w(ZW9INFbPxbnZrGf=pfaUPhzIIbNw(v13MNrTw1R9^!;P`ysIDHB{-?U z6FYIp{}bR;nY>r&*SH+rc3xSNWoR07l_>$jBUwxQS?@*I=*xQT@6z>ME#AU=EZ@T% zBZlW47>d2JnJO?tZ7Bq74=M(~;`%6fxuFhPwp0V#_J=={c z1iTf&Z|XC=3uj!{T}AtLXRpq(n4NfSm1{NAUG)^JpaKB;XXhk$^fkG9Hs0Wv6E|}0 zvm9HWT@w4+Vc5a0I@VyU!cxjSf>1Z7>#HranNZnd(s8H=@L@lt(^iU2tY!l)TjG!futm#f^(iLq4EEwI z2H$3;QhH3`p2`I}Nj)<|hLsPV`9a>IToc3}zI{_uXQ)F+Es?e#3aVsGSXs&Jgf+M^ ze6BQp5#|TI63)~%Gq{)T?sw1>V4_bXS(6ds?hRTdwSL1ly~N z1*%x1tIR^|PUN9*P>2n;yetQ@d?IrAF5ph)zukkK*V4#5P|i&XohwEK=FeQ2%+$z#De_S4>pHT=xr(-#-tM&wK!Nn11SAf$4^mWP{AR^oL2g9&vt>-N#9>%!nw5io8CfsTG^gA;wetO+Na~Y=>TQhQ< zq^5-D6N}*&=bB`^T$Ii8NUT{?>*B2x)V9QeVi@fIRENIZ*aZ(N%r|$4GmGNdrv!ac z!lga10hQYQq(8EVuXY>Sbm$nnfk0Roo@H54&l+qM1BexE!oYX@;A6^_cc0eR&S?)D z4m<#laF1p~SA{q(4C6{V&mUMwBKzqns}+a4e@GO*&GBmN_TW%GmJMPt1we5}>)@ei z`eppng}LJAVf2cKrDx!jP^)W-f!tA><5Sjs#*wbNTA#-b7P?@Mm{iiHaxgwHAutIB z1uG5%0vZJX1Qhb*rKqI((Ds&Vn>Zg#Kc;Ff=WYZPRYmC@Clom}lT+dUAe5L1%pVD- I0s{etpe5ti2><{9 diff --git a/natter-api/as.example.com.p12 b/natter-api/as.example.com.p12 deleted file mode 100644 index 0000acc2f8cdf455e6fd48dc768d4888a543177d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1506 zcmV<81s(b@f(70J0Ru3C1(ya1Duzgg_YDCD0ic2fhy;QKgfM~yfG~mudM)9z`A6$bGR8r{9q>>NiXJ^-u!Ua}dd2zNx+_>Ej1 zvne`^YR7X#A}J$jNlb zb{Iyw{8Cw^6|?jsmEbbCTPZIR(b>Wm;oG%kMUrU{Ai@~5N^Py4hT2-S<}=sBwlJrC z#lBTVq@x)JzkUuzSr2qy%HOi$PY<9MNpoQvZabKYb+{hUk}@rjn0+Rdhjedk7F$pe z?5g1gKL_i;%B5h44J<4sZ=E{LMw1k#CiEwVQi`)5&rA_!X~UQXb#RdeYRlVsMv}{5 zp*`WW2#4zrOp`;=R}&6R13Lk{PfFnASRLNxnYrK5F?myBwnjHOv$x!l`ufG($Fm109k!+ zy?of)YxZjah(8Xwf|IPJa-cD<-p-GEa8A{HxNd^g2#50|Z5c_#RS_qE^(Xzr4()nS zE`Ay+m~~fa`bjP`!CYLsk6E9d`5NQuZbk&bJ=_R9IRxL11eaW{^`XJhvI>I*YznE^ zDLk~leh?xkc5;1I7>nkv9930$mnVxU@+ONM#`1=e|3Dj++UxZ<{&=}CT1_f(ctv#% z5xp^OS^o1(924=!YSD?F@z%D$(LQ&1ExU(Uz48UAQiJiR8OY@hjsqD`dPT-!_TM!VkG# z`|(~_dhbg?KHpg2cC;ZHWe?Dw396H}4V2!Lt%~}gu@|9ycJ?^(Vz{==aHncCZ}Rda zZQYTDoj+00GirUgnSa104qbJl=9ppwxz+6Lg}6oh`EUAc_j|G81U(&vB+#(>UnEw4faZ;#WyP-YE206}ZytAZ zAFgg`#=MNRqt!?HzaO?g`vM3=`a%MTO03TjQZwX)X#u}Q?xDtsw>T`VUKvNO2%jIl zUU%x8R~QvPEr(BThi~YHYk^OUQC9KHQq%U$jj;Oru-v8!7F8;V}+fAqXNqpKAaE z;qArDaM;ntqN5bKTC~hWq!Wk=kq^H;sK|WInhL#b5 zkI1!95NZ(Gf*dh67J4TOy#l+jiiPd4hC-u$kN%5;pdGSjdla%YtAm!knJ_*uAutIB z1uG5%0vZJX1Qe6u5LqxE_o+xLWR&ybXHOj7spSL|03+#jN~Dh7Qb*W&@lKmhHYW1D I0s{etp!0*qE&u=k diff --git a/natter-api/docker/h2/Dockerfile b/natter-api/docker/h2/Dockerfile deleted file mode 100644 index aa0196a..0000000 --- a/natter-api/docker/h2/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM curlimages/curl:7.66.0 AS build-env - -ENV RELEASE h2-2018-03-18.zip -ENV SHA256 a45e7824b4f54f5d9d65fb89f22e1e75ecadb15ea4dcf8c5d432b80af59ea759 - -WORKDIR /tmp - -RUN echo "$SHA256 $RELEASE" > $RELEASE.sha256 && \ - curl -sSL https://www.h2database.com/$RELEASE -o $RELEASE && \ - sha256sum -b -c $RELEASE.sha256 && \ - unzip $RELEASE && rm -f $RELEASE - -FROM gcr.io/distroless/java:11 -WORKDIR /opt -COPY --from=build-env /tmp/h2/bin /opt/h2 - -USER 1000:1000 - -EXPOSE 9092 -ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/urandom", \ - "-cp", "/opt/h2/h2-1.4.197.jar", \ - "org.h2.tools.Server", "-tcp", "-tcpAllowOthers"] \ No newline at end of file diff --git a/natter-api/keystore.p12 b/natter-api/keystore.p12 deleted file mode 100644 index 246dee9ad645e9da06ee542cf1132126cc14ad39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2145 zcmbW%dpMNa9tZGuF2=}ZYfOlaF;dB;{k~%`6K6W*HnrW2YiV+sh(v70B^@&ub}{9i zQVr!cVbE?GLhNy^a};{haeWyXXA1p69pLde-`^@9$Y48|?*$ zAwV`-86~63p>mewVbU-j8!e4wqa|hOPaqp<_=hQ^Hj<6hl$1AttZtO-mrZi(U{bso zFluc9&O3ti2N~dY)Vlwyk{qRu#O9gvcjj?1c$Fom*T=@-q5Yxt9&iL8c0iISqGy6d zw3L&ZfNKO#@q07N)ipnsG`9S_TUC6~Ou4(O&KhN(NF{efMCEI@C6KjAbYHsAw0mE| zY~He{P4)rMv1Izu1$!%%wt&vR`WnVYG@q~{MnZeQzkIzg7E;tukZ2e{_&0cnCMKW> z83I98@BimX#+Xq$N{jcNwr7e)q{)x2ikAw%coM0s>H@j~OF(bUvD$3ASouqL&P{)s zR~{8_w^Als?LMiFsY!ao&`f7Uw6m4l>Q$9W@6T1nKvru5z(QCJj3l9OzzBcRW4q^O#`7f43OwTJ(Y}WD;vuNy}fg*dE`!R8a750JW*U=E(=&4LHX= z#JlMUWurDxB@IuJ<}-75msvecae*q_^XpcWtb5AZIC9JOAWpmdC*c{riZaSA{d=tk z+`zPioY!eFFC3tK;Fh%kscI-o;`!Wep%Rc0VEkt&6EcxVG$BF+0uf~0{|1zqv)@XC zRqc32%0BAq@t=1$ZKvCOLGh+@=voQWrcXU&WyDj29x2lvOFP@;rgnzldYabpj8b2x z7rZB~`U%-ZRUui$!Ro_(x8A-i-Etk>5|(c=Y;z?a*f*VV z6G1kj<@d*tM)BB)8(^D;n)}jOY`>e%+CD@AZzV#25SkrMaMU zm6xOZk3KeF7s}$;24)BM*P14`SlY`!7OLtDN_9-o8$Njq1hFFl*1Jzi51yZLn;EHv z)wk4Gr=4V0X;6q8KqtkqT?=$AkxL<;dZ=dsgH9j?PoXq*`rUtHoMp;4u7}&#dGK0O zezW8bT2VRqnbYD_S2vvwt^2zWs}8buRLG*ls!n6h;*{B?K+R*ZG4baKJ_Op05PYMH zL3p-WWicA-wQYz^7bObC%AF{JwL=l|hw390fA7$mu*ky|z*AOR_I@C(?4{skl{U^{ z0-UF=5r4o9Ym7&%zaWZLtb;eLnuvQkk`tXw{Z1Ax+(DIW9BDz`!8}jTo#z@NVOy^` zn~e?FWV&*{Q`jFE80K)MVd1Q&{<*(3RV>^G6eN zBup@(>umdSK+Vp~SUGz(>CA&SLeZhv(Y>dq6RG0fshB(OrJJj)2%Ql@3$N74W!WJ^-JfS!DJL8aZU_57)0w-M1$kNR=PvTyNB8vSa;gsX zYF~2AIBD%X+F&7hs6yT~zJGYMRGF7Gd9gSB z+3}A2B`OtGOxV8zo7-9BnL0AGBxZ(;Sbr2wpJ%#!f7Fq*IJl!SN9XYy0e`3u)8Y3^ zsC25deG%Jp3oCV5t=>J-*(*)eKOl3r&5dm1Usac;gtJkim{F!~YEFRg7{@Cje>MGf z-55v#@t_P!N>2d+SCayeSQYR2vf_z%2$`95`k=3?%Y;31D-zpTW}z18Rdg^z7mnR* QtNuYmT&{&9U>x2*0RZ<`kpKVy diff --git a/natter-api/kubernetes/natter-api-deployment.yaml b/natter-api/kubernetes/natter-api-deployment.yaml deleted file mode 100644 index 0df017d..0000000 --- a/natter-api/kubernetes/natter-api-deployment.yaml +++ /dev/null @@ -1,37 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: natter-api-deployment - namespace: natter-api -spec: - selector: - matchLabels: - app: natter-api - replicas: 1 - template: - metadata: - labels: - app: natter-api - spec: - securityContext: - runAsNonRoot: true - containers: - - name: natter-api - image: apisecurityinaction/natter-api:latest - imagePullPolicy: Never - volumeMounts: - - name: db-password - mountPath: "/etc/secrets/database" - readOnly: true - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - all - ports: - - containerPort: 4567 - volumes: - - name: db-password - secret: - secretName: db-password \ No newline at end of file diff --git a/natter-api/kubernetes/natter-api-service.yaml b/natter-api/kubernetes/natter-api-service.yaml deleted file mode 100644 index 25b0431..0000000 --- a/natter-api/kubernetes/natter-api-service.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: natter-api-service - namespace: natter-api -spec: - type: NodePort - selector: - app: natter-api - ports: - - protocol: TCP - port: 4567 - nodePort: 30567 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-database-deployment.yaml b/natter-api/kubernetes/natter-database-deployment.yaml deleted file mode 100644 index 763aebd..0000000 --- a/natter-api/kubernetes/natter-database-deployment.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: natter-database-deployment - namespace: natter-api -spec: - selector: - matchLabels: - app: natter-database - replicas: 1 - template: - metadata: - labels: - app: natter-database - spec: - securityContext: - runAsNonRoot: true - containers: - - name: natter-database - image: apisecurityinaction/h2database:latest - imagePullPolicy: Never - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - all - ports: - - containerPort: 9092 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-database-service.yaml b/natter-api/kubernetes/natter-database-service.yaml deleted file mode 100644 index 322c1b0..0000000 --- a/natter-api/kubernetes/natter-database-service.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: natter-database-service - namespace: natter-api -spec: - selector: - app: natter-database - ports: - - protocol: TCP - port: 9092 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-ingress.yaml b/natter-api/kubernetes/natter-ingress.yaml deleted file mode 100644 index 257ea09..0000000 --- a/natter-api/kubernetes/natter-ingress.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - name: api-ingress - namespace: natter-api - annotations: - nginx.ingress.kubernetes.io/upstream-vhost: - "$service_name.$namespace.svc.cluster.local:$service_port" - nginx.ingress.kubernetes.io/auth-tls-verify-client: "optional" - nginx.ingress.kubernetes.io/auth-tls-secret: "natter-api/ca-secret" - nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1" - nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: - "true" -spec: - tls: - - hosts: - - api.natter.local - secretName: natter-tls - rules: - - host: api.natter.local - http: - paths: - - backend: - serviceName: natter-api-service - servicePort: 4567 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-link-preview-deployment.yaml b/natter-api/kubernetes/natter-link-preview-deployment.yaml deleted file mode 100644 index 4100f4a..0000000 --- a/natter-api/kubernetes/natter-link-preview-deployment.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: link-preview-deployment - namespace: natter-api -spec: - selector: - matchLabels: - app: link-preview - replicas: 1 - template: - metadata: - labels: - app: link-preview - spec: - securityContext: - runAsNonRoot: true - containers: - - name: link-preview - image: apisecurityinaction/link-preview:latest - imagePullPolicy: Never - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - all - ports: - - containerPort: 4567 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-link-preview-service.yaml b/natter-api/kubernetes/natter-link-preview-service.yaml deleted file mode 100644 index d06e18b..0000000 --- a/natter-api/kubernetes/natter-link-preview-service.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: natter-link-preview-service - namespace: natter-api -spec: - selector: - app: link-preview - ports: - - protocol: TCP - port: 4567 \ No newline at end of file diff --git a/natter-api/kubernetes/natter-namespace.yaml b/natter-api/kubernetes/natter-namespace.yaml deleted file mode 100644 index 6846664..0000000 --- a/natter-api/kubernetes/natter-namespace.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: natter-api - labels: - name: natter-api - annotations: - linkerd.io/inject: enabled diff --git a/natter-api/localhost.p12 b/natter-api/localhost.p12 deleted file mode 100644 index 3a8322f5305cbb79f7aed716437ebc6451479ede..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3975 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 diff --git a/natter-api/natter-api-service.p12 b/natter-api/natter-api-service.p12 deleted file mode 100644 index 9f78b3b87386a1bf54b60091bd649bb59c6059f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4015 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 VEFsDk`fNpc1PHLQ$H%P(otidyq)z|< diff --git a/natter-api/natter-database-service.p12 b/natter-api/natter-database-service.p12 deleted file mode 100644 index f8d8dc7e0c00ae7a9b9fc8dd2f4db7429c223d10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4023 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 diff --git a/natter-api/natter-token-service.p12 b/natter-api/natter-token-service.p12 deleted file mode 100644 index 429a4496d233504c9b67ff9c3171ac1c984ef590..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4015 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 diff --git a/natter-api/pom.xml b/natter-api/pom.xml deleted file mode 100644 index 0f64d35..0000000 --- a/natter-api/pom.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - 4.0.0 - com.manning.api-security-in-action - natter-api - 1.0.0-SNAPSHOT - - 11 - 11 - - com.manning.apisecurityinaction.Main - - 7.26.0.Final - /etc/certs/natter-api/natter-api-service.p12 - - - - com.h2database - h2 - 1.4.197 - - - com.sparkjava - spark-core - 2.9.1 - - - org.json - json - 20180813 - - - org.dalesbred - dalesbred - 1.3.0 - - - org.slf4j - slf4j-simple - 1.7.26 - - - com.google.guava - guava - 27.0.1-jre - - - com.lambdaworks - scrypt - 1.4.0 - - - com.nimbusds - nimbus-jose-jwt - 8.3 - - - org.bouncycastle - bcpkix-jdk15on - 1.64 - - - org.bouncycastle - bctls-jdk15on - 1.64 - - - software.pando.crypto - salty-coffee - 1.0.2 - - - com.github.nitram509 - jmacaroons - 0.4.1 - - - com.augustcellars.cose - cose-java - 1.1.0 - - - org.cryptomator - siv-mode - 1.3.2 - - - - - org.kie - kie-api - ${drools.version} - - - org.drools - drools-core - ${drools.version} - - - org.drools - drools-compiler - ${drools.version} - - - - org.jsoup - jsoup - 1.12.1 - - - - - - - com.google.cloud.tools - jib-maven-plugin - 1.7.0 - - - apisecurityinaction/natter-api - - - gcr.io/distroless/java:11 - - - ${exec.mainClass} - - -Djava.security.egd=file:/dev/urandom - - - 4567 - - 1000:1000 - - - - - - \ No newline at end of file diff --git a/natter-api/server.pem b/natter-api/server.pem deleted file mode 100644 index 57f1306..0000000 --- a/natter-api/server.pem +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEVTCCAr2gAwIBAgIQbxKpXUlxdww7SpPlkp9sPzANBgkqhkiG9w0BAQsFADB5 -MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExJzAlBgNVBAsMHm5laWxA -Z3Vlc3Qycy1NYWNCb29rLVByby5sb2NhbDEuMCwGA1UEAwwlbWtjZXJ0IG5laWxA -Z3Vlc3Qycy1NYWNCb29rLVByby5sb2NhbDAeFw0xOTA1MTQxNDAxMjhaFw0yOTA1 -MTQxNDAxMjhaMGYxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZp -Y2F0ZTEnMCUGA1UECwwebmVpbEBndWVzdDJzLU1hY0Jvb2stUHJvLmxvY2FsMRIw -EAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQC1uG++EhRXI07PqEU3mUy1Nh44w+5LTV48UvPm1wQSweigf8H3OldBqanl6f6V -WcElGRgg5kJTI/Q6Vst83Aq7d5mSCEx6x1Qxdymk/4Qmk8kbxXNXcQbLbpK7ubzp -JTkAIVlp8FaSEumWzlBH5PYvCE5Md2G6A/j6HviYGdqd2WPQ9asRNaPzZQ1pNYuj -+efY3mXZrHCC130D4WLGEkjOvpip/NxkTDfT8bTUBJwktPhAfMWox9LPb+dcUwVy -ovHqzGtQ8+lk4wC2xlZKJOkgCAnl/C+rre0wCUuDM3ZJIGtUAJPldwhKCRreyVR5 -UwuYJBuVf82I5WvxxZS6GqNnAgMBAAGjbDBqMA4GA1UdDwEB/wQEAwIFoDATBgNV -HSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFNfG9Ux4 -W+6l222e9FCr/mUQ3uQuMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0B -AQsFAAOCAYEAo0vnIX0TAqVeDvLPm4F9nG/ZV6WwRQjn82napuhWTTn1wL/99Fm9 -YyweM2O5AbF4oBNqPGNuGcGEGrmBTlCZQmPGFp98dq1I+iAIRkAabUAeLYJ9SsT4 -GS8o0rzEKhVYYfovs9cNVGRSL9rmVvVP1npA/W2+wZ8yJOEzSY8mYMbzXd+/uU83 -sRifuofEuzZk/NY+h16T27PMTN+bgYsPZManNeMoKejH7R7a6ksqXmSApL/dPL7O -fI5DIjAam2+y8QzlqJGI+PzMsHZXVeWxI4acywjSINxsQrdVhukcgLfSMeNE/nRv -5SLWTDQksKzCcrIQg5Is55FH50W6mAeGbeDBKV9488PihIeDF3SKwHbVKICuK2sQ -5gIPVMbdFDYma8QyQwPdpDo7FKAhtDIL4ktaJMYBq1qPt6GbZhGweJlysXYtBTcf -r9l50PMjd3wKK7YxApnfYBfuadP5AVd7A/kHmvn+GlroTVTr0XzKcYI5Sk1HWIcv -C0HoMYDueVuj ------END CERTIFICATE----- diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/AesSivExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/AesSivExample.java deleted file mode 100644 index 1f06680..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/AesSivExample.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.manning.apisecurityinaction; - -import com.upokecenter.cbor.CBORObject; -import org.cryptomator.siv.SivMode; - -import javax.crypto.spec.SecretKeySpec; -import java.security.SecureRandom; -import java.util.Arrays; - -public class AesSivExample { - public static void main(String... args) throws Exception { - var psk = PskServer.loadPsk("changeit".toCharArray()); - var macKey = new SecretKeySpec(Arrays.copyOfRange(psk, 0, 16), - "AES"); - var encKey = new SecretKeySpec(Arrays.copyOfRange(psk, 16, 32), - "AES"); - - var randomIv = new byte[16]; - new SecureRandom().nextBytes(randomIv); - var header = "Test header".getBytes(); - var body = CBORObject.NewMap() - .Add("sensor", "F5671434") - .Add("reading", 1234).EncodeToBytes(); - - var siv = new SivMode(); - var ciphertext = siv.encrypt(encKey, macKey, body, - header, randomIv); - var plaintext = siv.decrypt(encKey, macKey, ciphertext, - header, randomIv); - assert Arrays.equals(plaintext, body); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java deleted file mode 100644 index 95e578e..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/CorsFilter.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.manning.apisecurityinaction; - -import spark.*; - -import java.util.Set; - -import static spark.Spark.halt; - -class CorsFilter implements Filter { - private final Set allowedOrigins; - - CorsFilter(Set allowedOrigins) { - this.allowedOrigins = allowedOrigins; - } - - @Override - public void handle(Request request, Response response) { - var origin = request.headers("Origin"); - if (origin != null && allowedOrigins.contains(origin)) { - response.header("Access-Control-Allow-Origin", origin); - response.header("Vary", "Origin"); - } - - if (isPreflightRequest(request)) { - if (origin == null || !allowedOrigins.contains(origin)) { - halt(403); - } - - response.header("Access-Control-Allow-Headers", - "Content-Type, Authorization"); - response.header("Access-Control-Allow-Methods", - "GET, POST, DELETE"); - halt(204); - } - } - - private boolean isPreflightRequest(Request request) { - return "OPTIONS".equals(request.requestMethod()) && - request.headers().contains("Access-Control-Request-Method"); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java deleted file mode 100644 index 04bda27..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/CoseEncryptionExample.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.manning.apisecurityinaction; - -import COSE.*; -import com.manning.apisecurityinaction.token.Base64url; -import com.upokecenter.cbor.CBORObject; -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -import java.security.*; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class CoseEncryptionExample { - private static final SecureRandom random = new SecureRandom(); - - public static void main(String... args) throws Exception { - Security.addProvider(new BouncyCastleProvider()); - var keyMaterial = PskServer.loadPsk("changeit".toCharArray()); - - var recipient = new Recipient(); - var keyData = CBORObject.NewMap() - .Add(KeyKeys.KeyType.AsCBOR(), KeyKeys.KeyType_Octet) - .Add(KeyKeys.Octet_K.AsCBOR(), keyMaterial); - recipient.SetKey(new OneKey(keyData)); - recipient.addAttribute(HeaderKeys.Algorithm, - AlgorithmID.HKDF_HMAC_SHA_256.AsCBOR(), - Attribute.PROTECTED); - var nonce = new byte[16]; - random.nextBytes(nonce); - recipient.addAttribute(HeaderKeys.HKDF_Context_PartyU_nonce, - CBORObject.FromObject(nonce), Attribute.PROTECTED); - - var message = new EncryptMessage(); - message.SetContent("Hello, World!"); - message.addAttribute(HeaderKeys.Algorithm, - AlgorithmID.AES_CCM_16_128_128.AsCBOR(), - Attribute.PROTECTED); - message.addRecipient(recipient); - - message.encrypt(); - System.out.println(Base64url.encode(message.EncodeToBytes())); - // Print the CBOR structure of the message - System.out.println(message.EncodeToCBORObject()); - - // To decrypt - var receivedMessage = (EncryptMessage) Message.DecodeFromBytes(message.EncodeToBytes()); - // The COSE reference implementation uses == to test for recipient equality - // (a bug?) so make sure to pass the exact same reference, but add back the key material - // that was stripped when generating the message. - var self = receivedMessage.getRecipient(0); - self.SetKey(new OneKey(keyData)); - receivedMessage.decrypt(self); - System.out.println("Received: " + new String(receivedMessage.GetContent(), UTF_8)); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Device.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Device.java deleted file mode 100644 index 6850f19..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Device.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.manning.apisecurityinaction; - -import org.dalesbred.Database; -import org.dalesbred.annotation.DalesbredInstantiator; -import org.h2.jdbcx.JdbcConnectionPool; -import software.pando.crypto.nacl.SecretBox; - -import java.io.*; -import java.security.Key; -import java.util.Optional; - -public class Device { - final String deviceId; - final String manufacturer; - final String model; - final byte[] encryptedPsk; - - @DalesbredInstantiator - public Device(String deviceId, String manufacturer, - String model, byte[] encryptedPsk) { - this.deviceId = deviceId; - this.manufacturer = manufacturer; - this.model = model; - this.encryptedPsk = encryptedPsk; - } - - public byte[] getPsk(Key decryptionKey) { - try (var in = new ByteArrayInputStream(encryptedPsk)) { - var box = SecretBox.readFrom(in); - return box.decrypt(decryptionKey); - } catch (IOException e) { - throw new RuntimeException("Unable to decrypt PSK", e); - } - } - - static Database createDatabase(SecretBox encryptedPsk) throws IOException { - var pool = JdbcConnectionPool.create("jdbc:h2:mem:devices", - "devices", "password"); - var database = Database.forDataSource(pool); - - database.update("CREATE TABLE devices(" + - "device_id VARCHAR(30) PRIMARY KEY," + - "manufacturer VARCHAR(100) NOT NULL," + - "model VARCHAR(100) NOT NULL," + - "encrypted_psk VARBINARY(1024) NOT NULL)"); - - var out = new ByteArrayOutputStream(); - encryptedPsk.writeTo(out); - database.update("INSERT INTO devices(" + - "device_id, manufacturer, model, encrypted_psk) " + - "VALUES(?, ?, ?, ?)", "test", "example", "ex001", - out.toByteArray()); - - return database; - } - - static Optional find(Database database, String deviceId) { - return database.findOptional(Device.class, - "SELECT device_id, manufacturer, model, encrypted_psk " + - "FROM devices WHERE device_id = ?", deviceId); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceGrantClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceGrantClient.java deleted file mode 100644 index 44f5566..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceGrantClient.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.manning.apisecurityinaction; - -import org.json.JSONObject; -import java.net.*; -import java.net.http.*; -import java.net.http.HttpRequest.BodyPublishers; -import java.net.http.HttpResponse.BodyHandlers; -import java.util.concurrent.TimeUnit; -import static java.nio.charset.StandardCharsets.UTF_8; - -public class DeviceGrantClient { - private static final HttpClient httpClient = HttpClient.newHttpClient(); - - public static void main(String... args) throws Exception { - var clientId = "deviceGrantTest"; - var scope = "a b c"; - - // Make initial request to device authorization endpoint - var json = beginDeviceAuthorization(clientId, scope); - var deviceCode = json.getString("device_code"); - var interval = json.optInt("interval", 5); - System.out.println("Please open " + json.getString("verification_uri")); - System.out.println("And enter code:\n\t" + json.getString("user_code")); - System.out.println("I'm waiting!"); - - while (true) { - Thread.sleep(TimeUnit.SECONDS.toMillis(interval)); - json = pollAccessTokenEndpoint(clientId, deviceCode); - var error = json.optString("error", null); - if (error != null) { - switch (error) { - case "slow_down": - System.out.println("Slowing down"); - interval += 5; - break; - case "authorization_pending": - System.out.println("Still waiting!"); - break; - default: - System.err.println("Authorization failed: " + error); - System.exit(1); - break; - } - } else { - System.out.println("Access token: " + json.getString("access_token")); - break; - } - } - } - - private static JSONObject beginDeviceAuthorization( - String clientId, String scope) throws Exception { - var form = "client_id=" + URLEncoder.encode(clientId, UTF_8) + - "&scope=" + URLEncoder.encode(scope, UTF_8) + - "&response_type=device_code"; - var request = HttpRequest.newBuilder() - .header("Content-Type", - "application/x-www-form-urlencoded") - .uri(URI.create( - "https://as.example.com:8443/openam/oauth2/device/code")) - .POST(BodyPublishers.ofString(form)) - .build(); - var response = httpClient.send(request, BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - throw new RuntimeException("Bad response from AS: " + response.body()); - } - return new JSONObject(response.body()); - } - - private static JSONObject pollAccessTokenEndpoint( - String clientId, String deviceCode) throws Exception { - var form = "client_id=" + clientId + - "&grant_type=urn:ietf:params:oauth:grant-type:device_code" + - "&device_code=" + URLEncoder.encode(deviceCode, UTF_8); - - var request = HttpRequest.newBuilder() - .header("Content-Type", "application/x-www-form-urlencoded") - .uri(URI.create("https://as.example.com:8443/openam/oauth2/access_token")) - .POST(BodyPublishers.ofString(form)) - .build(); - var response = httpClient.send(request, BodyHandlers.ofString()); - return new JSONObject(response.body()); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceIdentityManager.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceIdentityManager.java deleted file mode 100644 index 65579f9..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DeviceIdentityManager.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.manning.apisecurityinaction; -import org.bouncycastle.tls.TlsPSKIdentityManager; -import org.dalesbred.Database; -import java.security.Key; -import static java.nio.charset.StandardCharsets.UTF_8; - -public class DeviceIdentityManager implements TlsPSKIdentityManager { - private final Database database; - private final Key pskDecryptionKey; - - public DeviceIdentityManager(Database database, Key pskDecryptionKey) { - this.database = database; - this.pskDecryptionKey = pskDecryptionKey; - } - - @Override - public byte[] getHint() { - return null; - } - - @Override - public byte[] getPSK(byte[] identity) { - var deviceId = new String(identity, UTF_8); - return Device.find(database, deviceId) - .map(device -> device.getPsk(pskDecryptionKey)) - .orElse(null); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java deleted file mode 100644 index 9ec220d..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsClient.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.manning.apisecurityinaction; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.FileInputStream; -import java.nio.file.*; -import java.security.KeyStore; - -import javax.net.ssl.*; - -import org.slf4j.*; - -public class DtlsClient { - private static final Logger logger = LoggerFactory.getLogger(DtlsClient.class); - - public static void main(String... args) throws Exception { - try (var channel = new DtlsDatagramChannel(getClientContext(), sslParameters()); - var in = Files.newBufferedReader(Paths.get("test.txt"))) { - logger.info("Connecting to localhost:54321"); - channel.connect("localhost", 54321); - - String line; - while ((line = in.readLine()) != null) { - logger.info("Sending packet to server: {}", line); - channel.send(line.getBytes(UTF_8)); - } - - logger.info("All packets sent"); - logger.info("Used cipher suite: {}", - channel.getSession().getCipherSuite()); - } - } - - private static SSLContext getClientContext() throws Exception { - var sslContext = SSLContext.getInstance("DTLS"); - - var trustStore = KeyStore.getInstance("PKCS12"); - trustStore.load(new FileInputStream("as.example.com.ca.p12"), - "changeit".toCharArray()); - - var trustManagerFactory = TrustManagerFactory.getInstance( - "PKIX"); - trustManagerFactory.init(trustStore); - - sslContext.init(null, trustManagerFactory.getTrustManagers(), - null); - return sslContext; - } - - private static SSLParameters sslParameters() { - var params = DtlsDatagramChannel.defaultSslParameters(); - params.setCipherSuites(new String[] { - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_EMPTY_RENEGOTIATION_INFO_SCSV" - }); - return params; - } - -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java deleted file mode 100644 index 94d6a1e..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsDatagramChannel.java +++ /dev/null @@ -1,248 +0,0 @@ -package com.manning.apisecurityinaction; - -import java.io.*; -import java.net.*; -import java.nio.*; -import java.nio.channels.DatagramChannel; - -import javax.net.ssl.*; -import javax.net.ssl.SSLEngineResult.*; - -import org.slf4j.*; - -/** - * A rudimentary wrapper around the {@link SSLEngine} low-level DTLS - * protocol state machine. - *

- * Note: this class doesn't attempt to handle timeouts, - * lost packets, retransmissions, buffer overflows, and many other details - * of a robust UDP-based protocol implementation. It implements enough - * to provide a guidance to DTLS usage in Java. When used as a server, - * this class only supports a single client at a time and will discard - * packets received from other concurrent clients. - */ -public class DtlsDatagramChannel implements Closeable { - private static final Logger logger = LoggerFactory.getLogger(DtlsDatagramChannel.class); - - private final DatagramChannel channel; - private final SSLContext sslContext; - private final SSLParameters sslParameters; - - private ByteBuffer netRecvBuffer; - private ByteBuffer netSendBuffer; - private ByteBuffer appBuffer; - - private SSLEngine sslEngine; - - public DtlsDatagramChannel(SSLContext sslContext, SSLParameters sslParameters) throws IOException { - this.channel = DatagramChannel.open(); - this.sslContext = sslContext; - this.sslParameters = sslParameters; - - this.netRecvBuffer = ByteBuffer.allocateDirect(2048); - } - - public DtlsDatagramChannel(SSLContext sslContext) throws IOException { - this(sslContext, defaultSslParameters()); - } - - public static SSLParameters defaultSslParameters() { - var params = new SSLParameters(); - params.setProtocols(new String[] { "DTLSv1.2" }); - params.setMaximumPacketSize(1500); - params.setEnableRetransmissions(true); - params.setEndpointIdentificationAlgorithm("HTTPS"); - return params; - } - - - public DtlsDatagramChannel bind(int port) throws IOException { - channel.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), port)); - return this; - } - - public DtlsDatagramChannel connect(String host, int port) throws IOException { - channel.connect(new InetSocketAddress(host, port)); - return this; - } - - public SSLSession getSession() { - return sslEngine.getSession(); - } - - public void send(byte[] data) throws IOException { - if (!channel.isConnected()) { - throw new IllegalStateException("Channel must be connected"); - } - - if (sslEngine == null) { - var socketAddr = ((InetSocketAddress) channel.getRemoteAddress()); - sslEngine = sslContext.createSSLEngine(socketAddr.getHostName(), socketAddr.getPort()); - sslEngine.setUseClientMode(true); - sslEngine.setSSLParameters(sslParameters); - - handshake(sslEngine); - } - - if (sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING) { - throw new IllegalStateException("DTLS handshake failed"); - } - - appBuffer.put(data); - appBuffer.flip(); - var result = sslEngine.wrap(appBuffer, netSendBuffer); - appBuffer.compact(); - - if (result.getStatus() != Status.OK) { - throw new IllegalStateException("Wrap failed: " + result); - } - - netSendBuffer.flip(); - channel.write(netSendBuffer); - netSendBuffer.compact(); - } - - public InetSocketAddress receive(ByteBuffer buffer) throws IOException { - var address = (InetSocketAddress) channel.receive(netRecvBuffer); - if (!channel.isConnected()) { - channel.connect(address); - } - if (sslEngine == null) { - sslEngine = sslContext.createSSLEngine(address.getHostName(), address.getPort()); - sslEngine.setUseClientMode(false); - sslEngine.setSSLParameters(sslParameters); - - handshake(sslEngine); - channel.receive(netRecvBuffer); - } - - netRecvBuffer.flip(); - var result = sslEngine.unwrap(netRecvBuffer, buffer); - netRecvBuffer.compact(); - - if (result.getStatus() == Status.BUFFER_UNDERFLOW) { - throw new BufferUnderflowException(); - } - if (result.getStatus() == Status.BUFFER_OVERFLOW) { - throw new BufferOverflowException(); - } - if (result.getStatus() == Status.CLOSED) { - logger.info("Client disconnected"); - sslEngine.closeInbound(); - processEngineLoop(sslEngine); - sslEngine.closeOutbound(); - channel.disconnect(); - sslEngine = null; - } - - return address; - } - - @Override - public void close() throws IOException { - sslEngine.closeOutbound(); - // We should be able to call processEngineLoop here, but in OpenJDK 13 - // it erroneously returns HANDSHAKE_DONE when we are still waiting for - // the other side's close_notify alert. - - // Send close_notify alert - appBuffer.flip(); - sslEngine.wrap(appBuffer, netSendBuffer); - appBuffer.compact(); - netSendBuffer.flip(); - if (netSendBuffer.hasRemaining()) { - channel.write(netSendBuffer); - netSendBuffer.compact(); - } - - // Wait for close_notify response - while (!sslEngine.isInboundDone()) { - channel.receive(netRecvBuffer); - netRecvBuffer.flip(); - sslEngine.unwrap(netRecvBuffer, appBuffer); - netRecvBuffer.compact(); - } - sslEngine.closeInbound(); - channel.close(); - } - - private void handshake(SSLEngine engine) throws IOException { - if (!channel.isConnected()) { - throw new IllegalStateException("Channel must be connected"); - } - logger.info("Beginning DTLS handshake"); - engine.beginHandshake(); - - appBuffer = ByteBuffer.allocateDirect(engine.getSession().getApplicationBufferSize()); - netSendBuffer = ByteBuffer.allocateDirect(engine.getSession().getPacketBufferSize()); - - processEngineLoop(engine); - } - - private void processEngineLoop(SSLEngine engine) throws IOException { - var status = engine.getHandshakeStatus(); - while (status != HandshakeStatus.FINISHED && status != HandshakeStatus.NOT_HANDSHAKING) { - SSLEngineResult result; - logger.debug("Handshake status: " + status); - switch (status) { - case NEED_UNWRAP: - if (netRecvBuffer.position() == 0) { - channel.receive(netRecvBuffer); - } - case NEED_UNWRAP_AGAIN: - netRecvBuffer.flip(); - result = engine.unwrap(netRecvBuffer, appBuffer); - netRecvBuffer.compact(); - logger.debug("Unwrap result: {}", result); - - while (result.getStatus() == Status.BUFFER_UNDERFLOW) { - netRecvBuffer = ensureCapacity(netRecvBuffer, - sslEngine.getSession().getPacketBufferSize()); - channel.receive(netRecvBuffer); - netRecvBuffer.flip(); - result = engine.unwrap(netRecvBuffer, appBuffer); - netRecvBuffer.compact(); - } - - status = result.getHandshakeStatus(); - break; - case NEED_TASK: - Runnable task; - while ((task = engine.getDelegatedTask()) != null) { - logger.debug("Running task: {}", task); - task.run(); - } - status = engine.getHandshakeStatus(); - logger.debug("Tasks executed"); - break; - case NEED_WRAP: - appBuffer.flip(); - result = engine.wrap(appBuffer, netSendBuffer); - appBuffer.compact(); - logger.debug("Wrap result: " + result); - - netSendBuffer.flip(); - if (netSendBuffer.hasRemaining()) { - channel.write(netSendBuffer); - netSendBuffer.compact(); - } - status = result.getHandshakeStatus(); - break; - default: - throw new IllegalStateException( - "Unexpected handshake state: " + status); - } - } - logger.debug("Handshake finished!"); - } - - private ByteBuffer ensureCapacity(ByteBuffer buffer, int requiredSize) { - var remaining = buffer.remaining(); - if (remaining < requiredSize) { - var newBuffer = ByteBuffer.allocate(buffer.position() + requiredSize); - newBuffer.put(buffer.flip()); - return newBuffer; - } - return buffer; - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java deleted file mode 100644 index 53770aa..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/DtlsServer.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.manning.apisecurityinaction; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.FileInputStream; -import java.nio.ByteBuffer; -import java.security.KeyStore; - -import javax.net.ssl.*; - -import org.slf4j.*; - -public class DtlsServer { - private static final Logger logger = LoggerFactory.getLogger(DtlsServer.class); - - public static void main(String... args) throws Exception { - try (var channel = new DtlsDatagramChannel(getServerContext())) { - channel.bind(54321); - logger.info("Listening on port 54321"); - - var buffer = ByteBuffer.allocate(2048); - - while (true) { - channel.receive(buffer); - buffer.flip(); - var data = UTF_8.decode(buffer).toString(); - logger.info("Received: {}", data); - buffer.compact(); - } - } - } - - private static SSLContext getServerContext() throws Exception { - var sslContext = SSLContext.getInstance("DTLS"); - - var keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(new FileInputStream("localhost.p12"), - "changeit".toCharArray()); - - var keyManager = KeyManagerFactory.getInstance("PKIX"); - keyManager.init(keyStore, "changeit".toCharArray()); - - sslContext.init(keyManager.getKeyManagers(), null, null); - return sslContext; - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java b/natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java deleted file mode 100644 index 8c3c87d..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/HKDF.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.manning.apisecurityinaction; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.security.*; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Objects.checkIndex; - -public class HKDF { - public static Key extract(byte[] salt, byte[] inputKeyMaterial) - throws GeneralSecurityException { - var hmac = Mac.getInstance("HmacSHA256"); - if (salt == null) { - salt = new byte[hmac.getMacLength()]; - } - hmac.init(new SecretKeySpec(salt, "HmacSHA256")); - return new SecretKeySpec(hmac.doFinal(inputKeyMaterial), - "HmacSHA256"); - } - - public static Key expand(Key masterKey, String context, - int outputKeySize, String algorithm) - throws GeneralSecurityException { - return expand(masterKey, context.getBytes(UTF_8), - outputKeySize, algorithm); - } - - public static Key expand(Key masterKey, byte[] context, - int outputKeySize, String algorithm) - throws GeneralSecurityException { - - checkIndex(outputKeySize, 255*32); - - var hmac = Mac.getInstance("HmacSHA256"); - hmac.init(masterKey); - - var output = new byte[outputKeySize]; - var block = new byte[0]; - for (int i = 0; i < outputKeySize; i += 32) { - hmac.update(block); - hmac.update(context); - hmac.update((byte) ((i / 32) + 1)); - block = hmac.doFinal(); - System.arraycopy(block, 0, output, i, - Math.min(outputKeySize - i, 32)); - } - - return new SecretKeySpec(output, algorithm); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/JwtBearerClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/JwtBearerClient.java deleted file mode 100644 index 2f09ca7..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/JwtBearerClient.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.manning.apisecurityinaction; - -import java.io.FileInputStream; -import java.net.URI; -import java.net.http.*; -import java.security.KeyStore; -import java.security.interfaces.ECPrivateKey; -import java.util.*; - -import com.nimbusds.jose.*; -import com.nimbusds.jose.crypto.ECDSASigner; -import com.nimbusds.jose.jwk.*; -import com.nimbusds.jwt.*; - -import static java.time.Instant.now; -import static java.time.temporal.ChronoUnit.SECONDS; - -public class JwtBearerClient { - - public static void main(String... args) throws Exception { - var password = "changeit".toCharArray(); - var keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(new FileInputStream("keystore.p12"), - password); - var privateKey = (ECPrivateKey) keyStore.getKey("es256-key", - password); - - var jwk = ECKey.load(keyStore, "es256-key", password); - System.out.println("JWK Set:"); - System.out.println(new JWKSet(jwk.toPublicJWK())); - - var clientId = "test"; - var as = "https://as.example.com/access_token"; - var header = new JWSHeader(JWSAlgorithm.ES256); - var claims = new JWTClaimsSet.Builder() - .subject(clientId) - .issuer(clientId) - .expirationTime(Date.from(now().plus(30, SECONDS))) - .audience(as) - .jwtID(UUID.randomUUID().toString()) - .build(); - var jwt = new SignedJWT(header, claims); - jwt.sign(new ECDSASigner(privateKey)); - var assertion = jwt.serialize(); - System.out.println("Assertion: " + assertion); - - var form = "grant_type=client_credentials&scope=a+b+c" + - "&client_assertion_type=" + - "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + - "&client_assertion=" + assertion; - - var httpClient = HttpClient.newHttpClient(); - var request = HttpRequest.newBuilder() - .uri(URI.create(as)) - .header("Content-Type", - "application/x-www-form-urlencoded") - .POST(HttpRequest.BodyPublishers.ofString(form)) - .build(); - var response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString()); - System.out.println(response.statusCode()); - System.out.println(response.body()); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewer.java deleted file mode 100644 index 5a997d8..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/LinkPreviewer.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.manning.apisecurityinaction; - -import java.io.IOException; -import java.net.*; -import java.util.Set; - -import org.json.JSONObject; -import org.jsoup.*; -import org.jsoup.nodes.Document; -import org.slf4j.*; -import spark.ExceptionHandler; - -import static org.jsoup.Connection.Method.GET; -import static spark.Spark.*; - -public class LinkPreviewer { - private static final Logger logger = - LoggerFactory.getLogger(LinkPreviewer.class); - - public static void main(String...args) { - afterAfter((request, response) -> { - response.type("application/json; charset=utf-8"); - }); - - var expectedHostNames = Set.of( - "natter-link-preview-service:4567", - "natter-link-preview-service.natter-api:4567"); - before((request, response) -> { - if (!expectedHostNames.contains(request.host())) { - halt(400); - } - }); - - get("/preview", (request, response) -> { - var url = request.queryParams("url"); - var doc = fetch(url); - var title = doc.title(); - var desc = doc.head() - .selectFirst("meta[property='og:description']"); - var img = doc.head() - .selectFirst("meta[property='og:image']"); - - return new JSONObject() - .put("url", doc.location()) - .putOpt("title", title) - .putOpt("description", - desc == null ? null : desc.attr("content")) - .putOpt("image", - img == null ? null : img.attr("content")); - }); - - exception(IllegalArgumentException.class, handleException(400)); - exception(MalformedURLException.class, handleException(400)); - exception(Exception.class, handleException(502)); - exception(UnknownHostException.class, handleException(404)); - } - - private static Document fetch(String url) throws IOException { - Document doc = null; - var retries = 0; - while (doc == null && retries++ < 10) { - logger.info("Checking URL {}", url); - if (isBlockedAddress(url)) { - throw new IllegalArgumentException( - "URL refers to local/private address"); - } - var res = Jsoup.connect(url).followRedirects(false) - .timeout(3000).method(GET).execute(); - if (res.statusCode() / 100 == 3) { - url = res.header("Location"); - } else { - doc = res.parse(); - } - } - if (doc == null) throw new IOException("too many redirects"); - return doc; - } - - private static boolean isBlockedAddress(String uri) - throws UnknownHostException { - var host = URI.create(uri).getHost(); - for (var ipAddr : InetAddress.getAllByName(host)) { - if (ipAddr.isLoopbackAddress() || - ipAddr.isLinkLocalAddress() || - ipAddr.isSiteLocalAddress() || - ipAddr.isMulticastAddress() || - ipAddr.isAnyLocalAddress() || - isUniqueLocalAddress(ipAddr)) { - return true; - } - } - return false; - } - - private static boolean isUniqueLocalAddress(InetAddress ipAddr) { - return ipAddr instanceof Inet6Address && - (ipAddr.getAddress()[0] & 0xFF) == 0xFD && - (ipAddr.getAddress()[1] & 0xFF) == 0X00; - } - - private static ExceptionHandler - handleException(int status) { - return (ex, request, response) -> { - logger.error("Caught error {} - returning status {}", ex, status); - response.status(status); - response.body(new JSONObject().put("status", status).toString()); - }; - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java deleted file mode 100644 index 4dfba93..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Main.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.manning.apisecurityinaction; - -import java.io.FileInputStream; -import java.net.URI; -import java.nio.file.*; -import java.security.KeyStore; -import java.sql.Connection; -import java.util.Set; - -import com.google.common.util.concurrent.RateLimiter; -import com.manning.apisecurityinaction.controller.*; -import com.manning.apisecurityinaction.token.*; -import org.dalesbred.Database; -import org.dalesbred.result.EmptyResultException; -import org.h2.jdbcx.JdbcConnectionPool; -import org.json.*; -import software.pando.crypto.nacl.Crypto; -import spark.*; -import spark.embeddedserver.EmbeddedServers; -import spark.embeddedserver.jetty.EmbeddedJettyFactory; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static spark.Service.SPARK_DEFAULT_PORT; -import static spark.Spark.*; - -public class Main { - - public static void main(String... args) throws Exception { - EmbeddedServers.add(EmbeddedServers.defaultIdentifier(), - new EmbeddedJettyFactory().withHttpOnly(true)); - Spark.staticFiles.location("/public"); - port(args.length > 0 ? Integer.parseInt(args[0]) - : SPARK_DEFAULT_PORT); - - var secretsPath = Paths.get("/etc/secrets/database"); - var dbUsername = Files.readString(secretsPath.resolve("username")); - var dbPassword = Files.readString(secretsPath.resolve("password")); - - var jdbcUrl = "jdbc:h2:tcp://natter-database-service:9092/mem:natter"; - var datasource = JdbcConnectionPool.create( - jdbcUrl, dbUsername, dbPassword); - createTables(datasource.getConnection()); - datasource = JdbcConnectionPool.create( - jdbcUrl, "natter_api_user", "password"); - var database = Database.forDataSource(datasource); - - var keystore = KeyStore.getInstance("PKCS12"); - keystore.load(new FileInputStream("keystore.p12"), - "changeit".toCharArray()); - var macKey = keystore.getKey("hmac-key", "changeit".toCharArray()); - - // Examples of deriving keys using HKDF: - // Derive a symmetric AES key - var encKey = HKDF.expand(macKey, "token-encryption-key", - 32, "AES"); - - // Derive an Ed25519 signature key pair - var seed = HKDF.expand(macKey, "nacl-signing-key-seed", - 32, "NaCl"); - var keyPair = Crypto.seedSigningKeyPair(seed.getEncoded()); - - SecureTokenStore tokenStore = HmacTokenStore.wrap( - new DatabaseTokenStore(database), macKey); - var capController = new CapabilityController(tokenStore); - - var introspectionEndpoint = URI.create( - "http://as.example.com:8080/oauth2/instrospect"); - var oauthStore = new OAuth2TokenStore(introspectionEndpoint, - "rs", "password"); - var tokenController = new TokenController(oauthStore); - var spaceController = new SpaceController(database, capController); - var userController = new UserController(database); - - var rateLimiter = RateLimiter.create(2.0d); - before((request, response) -> { - if (!rateLimiter.tryAcquire()) { - halt(429); - } - }); - before(new CorsFilter(Set.of("https://localhost:9999"))); - - var expectedHostNames = Set.of( - "api.natter.local", - "api.natter.local:30567", - "natter-api-service:4567", - "natter-api-service.natter-api:4567", - "natter-api-service.natter-api.svc.cluster.local:4567" - ); - before((request, response) -> { - if (!expectedHostNames.contains(request.host())) { - halt(400); - } - }); - - before(((request, response) -> { - if (request.requestMethod().equals("POST") && - !"application/json".equals(request.contentType())) { - halt(415, new JSONObject().put( - "error", "Only application/json supported" - ).toString()); - } - })); - - before(userController::authenticate); - before(tokenController::validateToken); - - var auditController = new AuditController(database); - before(auditController::auditRequestStart); - afterAfter(auditController::auditRequestEnd); - - var droolsController = new DroolsAccessController(); - before("/*", droolsController::enforcePolicy); - - before("/sessions", userController::requireAuthentication); - post("/sessions", tokenController::login); - delete("/sessions", tokenController::logout); - - get("/logs", auditController::readAuditLog); - - post("/users", userController::registerUser); - - before("/spaces", userController::requireAuthentication); - before("/spaces", - tokenController.requireScope("POST", "create_space")); - post("/spaces", spaceController::createSpace); - - before("/spaces/:spaceId/messages", capController::lookupPermissions); - before("/spaces/:spaceId/messages/*", capController::lookupPermissions); - before("/spaces/:spaceId/members", capController::lookupPermissions); - - before("/spaces/*/messages", - tokenController.requireScope("POST", "post_message")); - before("/spaces/:spaceId/messages", - userController.requirePermission("POST", "w")); - post("/spaces/:spaceId/messages", spaceController::postMessage); - - before("/spaces/*/messages/*", - tokenController.requireScope("GET", "read_message")); - before("/spaces/:spaceId/messages/*", - userController.requirePermission("GET", "r")); - get("/spaces/:spaceId/messages/:msgId", - spaceController::readMessage); - - before("/spaces/*/messages", - tokenController.requireScope("GET", "list_messages")); - before("/spaces/:spaceId/messages", - userController.requirePermission("GET", "r")); - get("/spaces/:spaceId/messages", spaceController::findMessages); - - before("/spaces/*/members", - tokenController.requireScope("POST", "add_member")); - before("/spaces/:spaceId/members", - userController.requirePermission("POST", "rwd")); - post("/spaces/:spaceId/members", spaceController::addMember); - - var moderatorController = - new ModeratorController(database); - - before("/spaces/*/messages/*", - tokenController.requireScope("DELETE", "delete_message")); - before("/spaces/:spaceId/messages/*", - userController.requirePermission("DELETE", "d")); - delete("/spaces/:spaceId/messages/:msgId", - moderatorController::deletePost); - - afterAfter((request, response) -> { - response.type("application/json; charset=utf-8"); - response.header("X-Content-Type-Options", "nosniff"); - response.header("X-Frame-Options", "deny"); - response.header("X-XSS-Protection", "1; mode=block"); - response.header("Cache-Control", "private, max-age=0"); - response.header("Content-Security-Policy", - "default-src 'none'; frame-ancestors 'none'; sandbox"); - response.header("Server", ""); - }); - - internalServerError(new JSONObject() - .put("error", "internal server error").toString()); - notFound(new JSONObject() - .put("error", "not found").toString()); - - exception(IllegalArgumentException.class, Main::badRequest); - exception(JSONException.class, Main::badRequest); - exception(EmptyResultException.class, - (e, request, response) -> response.status(404)); - } - - private static void badRequest(Exception ex, - Request request, Response response) { - response.status(400); - response.body(new JSONObject().put("error", ex.getMessage()).toString()); - } - - static void createTables(Connection connection) throws Exception { - try (var conn = connection; - var stmt = conn.createStatement(); - var in = Main.class.getResourceAsStream("/schema.sql")) { - conn.setAutoCommit(false); - stmt.execute(new String(in.readAllBytes(), UTF_8)); - conn.commit(); - } - } -} \ No newline at end of file diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/NaclCborExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/NaclCborExample.java deleted file mode 100644 index 9f36bce..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/NaclCborExample.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.manning.apisecurityinaction; - -import com.upokecenter.cbor.CBORObject; -import software.pando.crypto.nacl.*; - -public class NaclCborExample { - public static void main(String... args) { - var senderKeys = CryptoBox.keyPair(); - var recipientKeys = CryptoBox.keyPair(); - var cborMap = CBORObject.NewMap() - .Add("foo", "bar") - .Add("data", 12345); - var sent = CryptoBox.encrypt(senderKeys.getPrivate(), - recipientKeys.getPublic(), cborMap.EncodeToBytes()); - - var recvd = CryptoBox.fromString(sent.toString()); - var cbor = recvd.decrypt(recipientKeys.getPrivate(), - senderKeys.getPublic()); - System.out.println(CBORObject.DecodeFromBytes(cbor)); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/Oscore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/Oscore.java deleted file mode 100644 index d27a116..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/Oscore.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.manning.apisecurityinaction; - -import COSE.*; -import com.upokecenter.cbor.CBORObject; -import org.apache.commons.codec.binary.Hex; -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -import java.nio.*; -import java.security.*; - -public class Oscore { - - private static Key deriveKey(Key hkdfKey, byte[] id, - byte[] idContext, AlgorithmID coseAlgorithm) - throws GeneralSecurityException { - - int keySizeBytes = coseAlgorithm.getKeySize() / 8; - CBORObject context = CBORObject.NewArray(); - context.Add(id); - context.Add(idContext); - context.Add(coseAlgorithm.AsCBOR()); - context.Add(CBORObject.FromObject("Key")); - context.Add(keySizeBytes); - - return HKDF.expand(hkdfKey, context.EncodeToBytes(), - keySizeBytes, "AES"); - } - - private static byte[] deriveCommonIV(Key hkdfKey, - byte[] idContext, AlgorithmID coseAlgorithm, int ivLength) - throws GeneralSecurityException { - CBORObject context = CBORObject.NewArray(); - context.Add(new byte[0]); - context.Add(idContext); - context.Add(coseAlgorithm.AsCBOR()); - context.Add(CBORObject.FromObject("IV")); - context.Add(ivLength); - - return HKDF.expand(hkdfKey, context.EncodeToBytes(), - ivLength, "dummy").getEncoded(); - } - - private static byte[] nonce(int ivLength, long sequenceNumber, - byte[] id, byte[] commonIv) { - if (sequenceNumber > (1L << 40)) - throw new IllegalArgumentException("Sequence number too large"); - int idLen = ivLength - 6; - if (id.length > idLen) - throw new IllegalArgumentException("ID is too large"); - - var buffer = ByteBuffer.allocate(ivLength).order(ByteOrder.BIG_ENDIAN); - buffer.put((byte) id.length); - buffer.put(new byte[idLen - id.length]); - buffer.put(id); - buffer.put((byte) ((sequenceNumber >>> 32) & 0xFF)); - buffer.putInt((int) sequenceNumber); - return xor(buffer.array(), commonIv); - } - - private static byte[] xor(byte[] xs, byte[] ys) { - for (int i = 0; i < xs.length; ++i) - xs[i] ^= ys[i]; - return xs; - } - - public static void main(String... args) throws Exception { - var algorithm = AlgorithmID.AES_CCM_16_64_128; - var masterKey = new byte[] { - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 - }; - var masterSalt = new byte[] { - (byte) 0x9e, 0x7c, (byte) 0xa9, 0x22, 0x23, 0x78, - 0x63, 0x40 - }; - var hkdfKey = HKDF.extract(masterSalt, masterKey); - var senderId = new byte[0]; - var recipientId = new byte[] { 0x01 }; - - var senderKey = deriveKey(hkdfKey, senderId, null, algorithm); - var recipientKey = deriveKey(hkdfKey, recipientId, null, algorithm); - var commonIv = deriveCommonIV(hkdfKey, null, algorithm, 13); - - System.out.println(Hex.encodeHex(senderKey.getEncoded())); - System.out.println(Hex.encodeHex(recipientKey.getEncoded())); - System.out.println(Hex.encodeHex(commonIv)); - - long sequenceNumber = 20L; - byte[] nonce = nonce(13, sequenceNumber, senderId, commonIv); - byte[] partialIv = new byte[] { (byte) sequenceNumber }; - - var message = new Encrypt0Message(); - message.addAttribute(HeaderKeys.Algorithm, - algorithm.AsCBOR(), Attribute.DO_NOT_SEND); - message.addAttribute(HeaderKeys.IV, - nonce, Attribute.DO_NOT_SEND); - message.addAttribute(HeaderKeys.PARTIAL_IV, - partialIv, Attribute.UNPROTECTED); - message.addAttribute(HeaderKeys.KID, - senderId, Attribute.UNPROTECTED); - message.SetContent( - new byte[] { 0x01, (byte) 0xb3, 0x74, 0x76, 0x31}); - - var associatedData = CBORObject.NewArray(); - associatedData.Add(1); - associatedData.Add(algorithm.AsCBOR()); - associatedData.Add(senderId); - associatedData.Add(partialIv); - associatedData.Add(new byte[0]); - message.setExternal(associatedData.EncodeToBytes()); - - Security.addProvider(new BouncyCastleProvider()); - message.encrypt(senderKey.getEncoded()); - System.out.println(Hex.encodeHex(message.getEncryptedContent())); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java deleted file mode 100644 index 99fb742..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskClient.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.manning.apisecurityinaction; - -import org.bouncycastle.tls.*; -import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; - -import java.net.*; -import java.nio.file.*; -import java.security.SecureRandom; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class PskClient { - public static void main(String[] args) throws Exception { - var psk = PskServer.loadPsk(args[0].toCharArray()); - var pskId = "test".getBytes(UTF_8); - - var crypto = new BcTlsCrypto(new SecureRandom()); - var client = new PSKTlsClient(crypto, pskId, psk) { - @Override - protected ProtocolVersion[] getSupportedVersions() { - return ProtocolVersion.DTLSv12.only(); - } - - @Override - protected int[] getSupportedCipherSuites() { - return new int[] { - CipherSuite.TLS_PSK_WITH_AES_128_CCM - }; - } - }; - - var address = InetAddress.getByName("localhost"); - var socket = new DatagramSocket(); - socket.connect(address, 54321); - socket.send(new DatagramPacket(new byte[0], 0)); - var transport = new UDPTransport(socket, 1500); - var protocol = new DTLSClientProtocol(); - var dtls = protocol.connect(client, transport); - - try (var in = Files.newBufferedReader(Paths.get("test.txt"))) { - String line; - while ((line = in.readLine()) != null) { - System.out.println("Sending: " + line); - var buf = line.getBytes(UTF_8); - dtls.send(buf, 0, buf.length); - } - } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java b/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java deleted file mode 100644 index 7721da2..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/PskServer.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.manning.apisecurityinaction; - -import org.bouncycastle.tls.*; -import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto; -import software.pando.crypto.nacl.SecretBox; - -import java.io.FileInputStream; -import java.net.*; -import java.security.*; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class PskServer { - public static void main(String[] args) throws Exception { - var psk = loadPsk(args[0].toCharArray()); - var encryptionKey = SecretBox.key(); - var deviceDb = Device.createDatabase( - SecretBox.encrypt(encryptionKey, psk)); - var crypto = new BcTlsCrypto(new SecureRandom()); - var server = new PSKTlsServer(crypto, - new DeviceIdentityManager(deviceDb, encryptionKey)) { - @Override - protected ProtocolVersion[] getSupportedVersions() { - return ProtocolVersion.DTLSv12.only(); - } - @Override - protected int[] getSupportedCipherSuites() { - return new int[] { - CipherSuite.TLS_PSK_WITH_AES_128_CCM, - CipherSuite.TLS_PSK_WITH_AES_128_CCM_8, - CipherSuite.TLS_PSK_WITH_AES_256_CCM, - CipherSuite.TLS_PSK_WITH_AES_256_CCM_8, - CipherSuite.TLS_PSK_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_PSK_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_PSK_WITH_CHACHA20_POLY1305_SHA256 - }; - } - - String getPeerDeviceIdentity() { - return new String(context.getSecurityParametersConnection() - .getPSKIdentity(), UTF_8); - } - }; - var buffer = new byte[2048]; - var serverSocket = new DatagramSocket(54321); - var packet = new DatagramPacket(buffer, buffer.length); - serverSocket.receive(packet); - serverSocket.connect(packet.getSocketAddress()); - - var protocol = new DTLSServerProtocol(); - var transport = new UDPTransport(serverSocket, 1500); - var dtls = protocol.accept(server, transport); - - while (true) { - var len = dtls.receive(buffer, 0, buffer.length, 60000); - if (len == -1) break; - System.out.println("Receiving data from device: " + - server.getPeerDeviceIdentity()); - var data = new String(buffer, 0, len, UTF_8); - System.out.println("Received: " + data); - } - } - - static byte[] loadPsk(char[] password) throws Exception { - var keyStore = KeyStore.getInstance("PKCS12"); - keyStore.load(new FileInputStream("keystore.p12"), password); - return keyStore.getKey("aes-key", password).getEncoded(); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/RatchetExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/RatchetExample.java deleted file mode 100644 index 618ff05..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/RatchetExample.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.manning.apisecurityinaction; - -import org.bouncycastle.util.encoders.Hex; - -import javax.crypto.Cipher; -import javax.crypto.spec.*; -import java.util.Arrays; - -public class RatchetExample { - - public static void main(String... args) throws Exception { - var key = PskServer.loadPsk(args[0].toCharArray()); - System.out.println("Original key: " + Hex.toHexString(key)); - for (int i = 0; i < 10; ++i) { - var newKey = ratchet(key); - Arrays.fill(key, (byte) 0); - key = newKey; - System.out.println("Next key: " + Hex.toHexString(key)); - } - } - - private static byte[] ratchet(byte[] oldKey) throws Exception { - var cipher = Cipher.getInstance("AES/CTR/NoPadding"); - var iv = new byte[16]; - Arrays.fill(iv, (byte) 0xFF); - cipher.init(Cipher.ENCRYPT_MODE, - new SecretKeySpec(oldKey, "AES"), new IvParameterSpec(iv)); - return cipher.doFinal(new byte[32]); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java b/natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java deleted file mode 100644 index 097c96f..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/ReplayProtectionExample.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.manning.apisecurityinaction; - -import com.upokecenter.cbor.CBORObject; -import software.pando.crypto.nacl.CryptoBox; - -import java.net.URI; -import java.net.http.*; -import java.security.KeyPair; -import java.util.concurrent.atomic.AtomicInteger; - -import static java.lang.Integer.parseInt; -import static java.net.http.HttpResponse.BodyHandlers.ofString; -import static spark.Spark.*; - -public class ReplayProtectionExample implements Runnable { - private static final KeyPair clientKeys = CryptoBox.keyPair(); - private static final KeyPair serverKeys = CryptoBox.keyPair(); - - public static void main(String... args) throws Exception { - new Thread(new ReplayProtectionExample()).start(); - - var revisionEtag = "42"; - var headers = CBORObject.NewMap() - .Add("If-Matches", revisionEtag); - var body = CBORObject.NewMap() - .Add("foo", "bar") - .Add("data", 12345); - var request = CBORObject.NewMap() - .Add("method", "PUT") - .Add("headers", headers) - .Add("body", body); - var sent = CryptoBox.encrypt(clientKeys.getPrivate(), - serverKeys.getPublic(), request.EncodeToBytes()); - - var httpRequest = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:4567/test")) - .header("If-Matches", revisionEtag) - .PUT(HttpRequest.BodyPublishers.ofString(sent.toString())) - .build(); - var httpResponse = HttpClient.newHttpClient().send(httpRequest, ofString()); - System.out.println("Received response: " + httpResponse.statusCode()); - System.out.println("ETag: " + httpResponse.headers().allValues("ETag")); - } - - @Override - public void run() { - - before((request, response) -> { - var encryptedRequest = CryptoBox.fromString(request.body()); - var decrypted = encryptedRequest.decrypt( - serverKeys.getPrivate(), clientKeys.getPublic()); - var cbor = CBORObject.DecodeFromBytes(decrypted); - - if (!cbor.get("method").AsString() - .equals(request.requestMethod())) { - halt(403); - } - - var expectedHeaders = cbor.get("headers"); - for (var headerName : expectedHeaders.getKeys()) { - if (!expectedHeaders.get(headerName).AsString() - .equals(request.headers(headerName.AsString()))) { - halt(403); - } - } - - request.attribute("decryptedRequest", cbor.get("body")); - }); - - // Simulate updating an ETag using an AtomicInteger. In a - // real example the ETag would be stored alongside the data - // and updated in a transaction. - var etag = new AtomicInteger(42); - put("/test", (request, response) -> { - var expectedEtag = parseInt(request.headers("If-Matches")); - - if (!etag.compareAndSet(expectedEtag, expectedEtag + 1)) { - response.status(412); - return null; - } - - System.out.println("Updating resource with new content: " + - request.attribute("decryptedRequest")); - - response.status(200); - response.header("ETag", String.valueOf(expectedEtag + 1)); - response.type("text/plain"); - return "OK"; - }); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java b/natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java deleted file mode 100644 index a8df03f..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/RevokeAccessToken.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.manning.apisecurityinaction; - -import java.net.*; -import java.net.http.*; -import java.net.http.HttpResponse.BodyHandlers; -import java.util.Base64; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class RevokeAccessToken { - - private static final URI revocationEndpoint = - URI.create("https://as.example.com:8443/oauth2/token/revoke"); - - public static void main(String...args) throws Exception { - - if (args.length != 3) { - throw new IllegalArgumentException( - "RevokeAccessToken clientId clientSecret token"); - } - - var clientId = args[0]; - var clientSecret = args[1]; - var token = args[2]; - - var credentials = URLEncoder.encode(clientId, UTF_8) + - ":" + URLEncoder.encode(clientSecret, UTF_8); - var authorization = "Basic " + Base64.getEncoder() - .encodeToString(credentials.getBytes(UTF_8)); - - var httpClient = HttpClient.newHttpClient(); - - var form = "token=" + URLEncoder.encode(token, UTF_8) + - "&token_type_hint=access_token"; - - var httpRequest = HttpRequest.newBuilder() - .uri(revocationEndpoint) - .header("Content-Type", - "application/x-www-form-urlencoded") - .header("Authorization", authorization) - .POST(HttpRequest.BodyPublishers.ofString(form)) - .build(); - - httpClient.send(httpRequest, BodyHandlers.discarding()); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java b/natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java deleted file mode 100644 index 71fe0b5..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/TokenService.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.manning.apisecurityinaction; - -import com.manning.apisecurityinaction.token.DatabaseTokenStore; -import com.manning.apisecurityinaction.token.TokenStore.Token; -import org.dalesbred.Database; -import org.h2.jdbcx.JdbcConnectionPool; -import org.json.JSONObject; -import org.slf4j.*; - -import static spark.Spark.*; - -public class TokenService { - private static final Logger logger = - LoggerFactory.getLogger(TokenService.class); - - public static void main(String... args) throws Exception { - secure("/etc/certs/natter-token-service/natter-token-service.p12", - "changeit", null, null); - var jdbcUrl = - "jdbc:h2:ssl://natter-token-database-service:9092/mem:tokens"; - var datasource = JdbcConnectionPool.create( - jdbcUrl, "natter", "password"); - Main.createTables(datasource.getConnection()); - datasource = JdbcConnectionPool.create( - jdbcUrl, "natter_api_user", "password"); - var database = Database.forDataSource(datasource); - var tokenStore = new DatabaseTokenStore(database); - - afterAfter((request, response) -> { - response.header("Content-Type", "application/json"); - }); - - post("/tokens", (request, response) -> { - var json = new JSONObject(request.body()); - var token = Token.fromJson(json); - var tokenId = tokenStore.create(request, token); - logger.info("Created token for user: {}", token.username); - return new JSONObject().put("tokenId", tokenId); - }); - - get("/tokens/:tokenId", (request, response) -> { - logger.info("Validating token"); - var tokenId = request.params(":tokenId"); - return tokenStore.read(request, tokenId) - .map(Token::toJson) - .orElseGet(() -> { - response.status(404); - return new JSONObject(); - }); - }); - - delete("/tokens/:tokenId", (request, response) -> { - var tokenId = request.params(":tokenId"); - logger.info("Revoking token: {}", tokenId); - tokenStore.revoke(request, tokenId); - return new JSONObject(); - }); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java deleted file mode 100644 index 5e0a4ad..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ABACAccessController.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import java.time.LocalTime; -import java.util.*; - -import spark.*; - -import static spark.Spark.halt; - -public abstract class ABACAccessController { - - public void enforcePolicy(Request request, Response response) { - - var subjectAttrs = new HashMap(); - subjectAttrs.put("user", request.attribute("subject")); - subjectAttrs.put("groups", request.attribute("groups")); - - var resourceAttrs = new HashMap(); - resourceAttrs.put("path", request.pathInfo()); - resourceAttrs.put("space", request.params(":spaceId")); - - var actionAttrs = new HashMap(); - actionAttrs.put("method", request.requestMethod()); - - var envAttrs = new HashMap(); - envAttrs.put("timeOfDay", LocalTime.now()); - envAttrs.put("ip", request.ip()); - - var permitted = checkPermitted(subjectAttrs, resourceAttrs, - actionAttrs, envAttrs); - - if (!permitted) { - halt(403); - } - } - - abstract boolean checkPermitted( - Map subject, - Map resource, - Map action, - Map env); - - public static class Decision { - private boolean permit = true; - - public void deny() { - permit = false; - } - - public void permit() { - } - - boolean isPermitted() { - return permit; - } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuditController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuditController.java deleted file mode 100644 index baeee2f..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuditController.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.stream.Collectors; - -import org.dalesbred.Database; -import org.json.*; - -import spark.*; - -public class AuditController { - - private final Database database; - - public AuditController(Database database) { - this.database = database; - } - - public void auditRequestStart(Request request, Response response) { - database.withVoidTransaction(tx -> { - var auditId = database.findUniqueLong( - "SELECT NEXT VALUE FOR audit_id_seq"); - request.attribute("audit_id", auditId); - database.updateUnique( - "INSERT INTO audit_log(audit_id, method, path, " + - "user_id, audit_time) " + - "VALUES(?, ?, ?, ?, current_timestamp)", - auditId, - request.requestMethod(), - request.pathInfo(), - request.attribute("subject")); - }); - } - public void auditRequestEnd(Request request, Response response) { - database.updateUnique( - "INSERT INTO audit_log(audit_id, method, path, status, " + - "user_id, audit_time) " + - "VALUES(?, ?, ?, ?, ?, current_timestamp)", - request.attribute("audit_id"), - request.requestMethod(), - request.pathInfo(), - response.status(), - request.attribute("subject")); - } - - public JSONArray readAuditLog(Request request, Response response) { - var since = Instant.now().minus(1, ChronoUnit.HOURS); - - var logs = database.findAll(LogRecord.class, - "SELECT audit_id, method, path, status, user_id, " + - "audit_time FROM audit_log WHERE audit_time >= ?", - since); - - return new JSONArray(logs.stream() - .map(LogRecord::toJson) - .collect(Collectors.toList())); - } - - public static class LogRecord { - private final Long auditId; - private final String method; - private final String path; - private final Integer status; - private final String user; - private final Instant auditTime; - - - public LogRecord(Long auditId, String method, String path, - Integer status, String user, Instant auditTime) { - this.auditId = auditId; - this.method = method; - this.path = path; - this.status = status; - this.user = user; - this.auditTime = auditTime; - } - - JSONObject toJson() { - return new JSONObject() - .put("id", auditId) - .put("method", method) - .put("path", path) - .put("status", status) - .put("user", user) - .put("time", auditTime.toString()); - } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuthorizationServerController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuthorizationServerController.java deleted file mode 100644 index 4f8800c..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/AuthorizationServerController.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import com.lambdaworks.crypto.SCryptUtil; -import com.manning.apisecurityinaction.token.*; -import org.dalesbred.Database; -import org.json.JSONObject; -import spark.*; - -import java.security.*; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.*; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class AuthorizationServerController { - - private final SecureTokenStore tokenStore; - private final Database database; - private final JSONObject clientConfig; - - public AuthorizationServerController(SecureTokenStore tokenStore, - Database database, - JSONObject clientConfig) { - this.tokenStore = tokenStore; - this.database = database; - this.clientConfig = clientConfig; - } - - public JSONObject issueAccessToken(Request request, Response response) { - - var grantType = request.queryMap("grant_type").value(); - if (!"password".equals(grantType)) { - throw new IllegalArgumentException("unsupported_grant_type"); - } - - var client = authenticateClient(request); - var username = request.queryMap("username").value(); - var password = request.queryMap("password").value(); - var scope = request.queryMap("scope").value(); - - if (scope == null || scope.isBlank() || - username == null || username.isBlank() || - password == null || password.isBlank()) { - throw new IllegalArgumentException("invalid_request"); - } - - if (!username.matches(UserController.USERNAME_PATTERN)) { - throw new IllegalArgumentException("invalid_request"); - } - scope = validateScope(scope, client); - - var hash = database.findOptional(String.class, - "SELECT pw_hash FROM users WHERE user_id = ?", username); - - if (hash.isPresent() && SCryptUtil.check(password, hash.get())) { - var expiry = Instant.now().plus(1, ChronoUnit.HOURS); - var token = new TokenStore.Token(expiry, username); - token.attributes.put("scope", scope); - - var tokenId = tokenStore.create(request, token); - return new JSONObject() - .put("access_token", tokenId) - .put("token_type", "Bearer") - .put("expires_in", 3600) - .put("scope", scope); - } else { - throw new IllegalArgumentException("invalid_grant"); - } - } - - private String validateScope(String scope, JSONObject client) { - var allowedScope = client.getJSONArray("allowed_scope").toList(); - var requestScope = scope.split(" "); - - var resultScope = new TreeSet(); - for (var requested : requestScope) { - if (allowedScope.contains(requested)) { - resultScope.add(requested); - } - } - - return String.join(" ", resultScope); - } - - private JSONObject authenticateClient(Request request) { - var clientId = request.queryMap("client_id").value(); - var secret = request.queryMap("client_secret").value(); - - if (clientId == null || clientId.isBlank() || - secret == null || secret.isBlank()) { - throw new IllegalArgumentException("invalid_client"); - } - - var client = clientConfig.optJSONObject(clientId); - if (client == null) { - throw new IllegalArgumentException("invalid_client"); - } - - var expected = Base64.getDecoder().decode( - client.getString("secret_hash")); - var provided = hash(secret); - - if (!MessageDigest.isEqual(expected, provided)) { - throw new IllegalArgumentException("invalid_client"); - } - - return client; - } - - public static byte[] hash(String clientSecret) { - try { - var sha256 = MessageDigest.getInstance("SHA-256"); - return sha256.digest(clientSecret.getBytes(UTF_8)); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java deleted file mode 100644 index ab58008..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/CapabilityController.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import java.net.URI; -import java.time.Instant; -import java.util.Objects; - -import com.manning.apisecurityinaction.token.SecureTokenStore; -import com.manning.apisecurityinaction.token.TokenStore.Token; -import spark.*; - -import static java.time.temporal.ChronoUnit.DAYS; - -public class CapabilityController { - private static final Instant NON_EXPIRING = - Instant.EPOCH.plus(10000 * 365, DAYS); - - private final SecureTokenStore tokenStore; - - public CapabilityController(SecureTokenStore tokenStore) { - this.tokenStore = tokenStore; - } - - public URI createUri(Request request, String path, String perms) { - var token = new Token(NON_EXPIRING, null); - token.attributes.put("path", path); - token.attributes.put("perms", perms); - - var tokenId = tokenStore.create(request, token); - - var base = URI.create(request.url()); - return base.resolve(path + "?access_token=" + tokenId); - } - - public void lookupPermissions(Request request, Response response) { - var tokenId = request.queryParams("access_token"); - if (tokenId == null) return; - - tokenStore.read(request, tokenId).ifPresent(token -> { - var tokenPath = token.attributes.get("path"); - if (Objects.equals(tokenPath, request.pathInfo())) { - request.attribute("perms", - token.attributes.get("perms")); - } - }); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/DroolsAccessController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/DroolsAccessController.java deleted file mode 100644 index aba92bd..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/DroolsAccessController.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import java.util.*; - -import org.kie.api.KieServices; -import org.kie.api.runtime.KieContainer; - -public class DroolsAccessController extends ABACAccessController { - - private final KieContainer kieContainer; - - public DroolsAccessController() { - this.kieContainer = KieServices.get().getKieClasspathContainer(); - } - - @Override - boolean checkPermitted(Map subject, - Map resource, - Map action, - Map env) { - - var session = kieContainer.newKieSession(); - try { - var decision = new Decision(); - session.setGlobal("decision", decision); - - session.insert(new Subject(subject)); - session.insert(new Resource(resource)); - session.insert(new Action(action)); - session.insert(new Environment(env)); - - session.fireAllRules(); - return decision.isPermitted(); - - } finally { - session.dispose(); - } - } - - public static class Subject extends HashMap { - Subject(Map m) { super(m); } - } - - public static class Resource extends HashMap { - Resource(Map m) { super(m); } - } - - public static class Action extends HashMap { - Action(Map m) { super(m); } - } - - public static class Environment extends HashMap { - Environment(Map m) { super(m); } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/IdTokenValidationFilter.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/IdTokenValidationFilter.java deleted file mode 100644 index 8df029d..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/IdTokenValidationFilter.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import com.nimbusds.jose.*; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.*; -import com.nimbusds.jwt.proc.DefaultJWTProcessor; -import spark.*; - -import java.text.ParseException; - -public class IdTokenValidationFilter implements Filter { - - private final String expectedIssuer; - private final String expectedAudience; - private final JWSAlgorithm signatureAlgorithm; - private final JWKSource jwkSource; - - public IdTokenValidationFilter(String expectedIssuer, - String expectedAudience, - JWSAlgorithm signatureAlgorithm, - JWKSource jwkSource) { - this.expectedIssuer = expectedIssuer; - this.expectedAudience = expectedAudience; - this.signatureAlgorithm = signatureAlgorithm; - this.jwkSource = jwkSource; - } - - @Override - public void handle(Request request, Response response) { - - var idToken = request.headers("X-ID-Token"); - if (idToken == null) return; - var subject = request.attribute("subject"); - if (subject == null) return; - - var verifier = new DefaultJWTProcessor<>(); - var keySelector = new JWSVerificationKeySelector<>( - signatureAlgorithm, jwkSource); - verifier.setJWSKeySelector(keySelector); - - try { - var claims = verifier.process(idToken, null); - - if (!expectedIssuer.equals(claims.getIssuer())) { - throw new IllegalArgumentException( - "invalid id token issuer"); - } - if (!claims.getAudience().contains(expectedAudience)) { - throw new IllegalArgumentException( - "invalid id token audience"); - } - - var client = request.attribute("client_id"); - var azp = claims.getStringClaim("azp"); - if (client != null && azp != null && !azp.equals(client)) { - throw new IllegalArgumentException( - "client is not authorized party"); - } - - if (!subject.equals(claims.getSubject())) { - throw new IllegalArgumentException( - "subject does not match id token"); - } - - request.attribute("id_token.claims", claims); - - } catch (ParseException | BadJOSEException | JOSEException e) { - throw new IllegalArgumentException("invalid id token", e); - } - - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/LdapUserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/LdapUserController.java deleted file mode 100644 index 7d93dac..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/LdapUserController.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import javax.naming.*; -import javax.naming.directory.*; -import java.util.*; - -import org.dalesbred.Database; -import org.json.JSONObject; -import org.slf4j.*; -import spark.*; - -public class LdapUserController extends UserController { - private static final Logger logger = - LoggerFactory.getLogger(LdapUserController.class); - - private final String ldapUrl; - private final String baseDn; - private final DirContext connection; - - public LdapUserController(Database database, String ldapUrl, - String baseDn, String connDn, - String connPassword) throws NamingException { - super(database); - this.ldapUrl = ldapUrl; - this.baseDn = baseDn; - this.connection = bind(connDn, connPassword); - } - - @Override - public JSONObject registerUser(Request request, Response response) { - throw new UnsupportedOperationException( - "Please register users in LDAP directly"); - } - - @Override - public void authenticate(Request request, Response response) { - var credentials = getCredentials(request); - if (credentials == null) return; - - var username = credentials[0]; - var password = credentials[1]; - - var dn = "uid=" + username + ",ou=people," + baseDn; - - try { - var directory = bind(dn, password); - // Authentication succeeded - request.attribute("subject", username); - directory.close(); - - // Lookup static groups for the user - var searchControls = new SearchControls(); - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - searchControls.setReturningAttributes(new String[] { "cn" }); - - var groups = new ArrayList(); - var results = connection.search("ou=groups," + baseDn, - "(&(objectClass=groupOfNames)(member={0}))", - new Object[]{ dn }, - searchControls); - try { - while (results.hasMore()) { - var result = results.next(); - groups.add((String) result.getAttributes() - .get("cn").get(0)); - } - } finally { - results.close(); - } - request.attribute("groups", groups); - } catch (AuthenticationException e) { - logger.debug("Authentication failed for user {}", username, e); - } catch (NamingException e) { - throw new RuntimeException("Unable to login", e); - } - } - - private DirContext bind(String userDn, String password) - throws NamingException { - var props = new Properties(); - props.put(Context.INITIAL_CONTEXT_FACTORY, - "com.sun.jndi.ldap.LdapCtxFactory"); - props.put(Context.PROVIDER_URL, ldapUrl); - props.put(Context.SECURITY_AUTHENTICATION, "simple"); - props.put(Context.SECURITY_PRINCIPAL, userDn); - props.put(Context.SECURITY_CREDENTIALS, password); - - return new InitialDirContext(props); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ModeratorController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ModeratorController.java deleted file mode 100644 index 294c4a5..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/ModeratorController.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import org.dalesbred.Database; -import org.json.JSONObject; - -import spark.*; - -public class ModeratorController { - - private final Database database; - - public ModeratorController(Database database) { - this.database = database; - } - - public JSONObject deletePost(Request request, Response response) { - var spaceId = Long.parseLong(request.params(":spaceId")); - var msgId = Long.parseLong(request.params(":msgId")); - - database.updateUnique("DELETE FROM messages " + - "WHERE space_id = ? AND msg_id = ?", spaceId, msgId); - response.status(200); - return new JSONObject(); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java deleted file mode 100644 index 6562947..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/SpaceController.java +++ /dev/null @@ -1,225 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import java.net.*; -import java.net.http.*; -import java.net.http.HttpResponse.BodyHandlers; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.*; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.dalesbred.Database; -import org.json.*; -import spark.*; - -public class SpaceController { - private static final Set DEFINED_ROLES = - Set.of("owner", "moderator", "member", "observer"); - - private final Database database; - private final CapabilityController capabilityController; - - public SpaceController(Database database, - CapabilityController capabilityController) { - this.database = database; - this.capabilityController = capabilityController; - } - - public JSONObject createSpace(Request request, Response response) { - var json = new JSONObject(request.body()); - var spaceName = json.getString("name"); - if (spaceName.length() > 255) { - throw new IllegalArgumentException("space name too long"); - } - var owner = json.getString("owner"); - if (!owner.matches("[a-zA-Z][a-zA-Z0-9]{1,29}")) { - throw new IllegalArgumentException("invalid username"); - } - var subject = request.attribute("subject"); - if (!owner.equals(subject)) { - throw new IllegalArgumentException( - "owner must match authenticated user"); - } - - return database.withTransaction(tx -> { - var spaceId = database.findUniqueLong( - "SELECT NEXT VALUE FOR space_id_seq;"); - - database.updateUnique( - "INSERT INTO spaces(space_id, name, owner) " + - "VALUES(?, ?, ?);", spaceId, spaceName, owner); - - var uri = capabilityController.createUri(request, - "/spaces/" + spaceId, "rwd"); - var messagesUri = capabilityController.createUri(request, - "/spaces/" + spaceId + "/messages", "rwd"); - var messagesReadWriteUri = capabilityController.createUri( - request, "/spaces/" + spaceId + "/messages", "rw"); - var messagesReadOnlyUri = capabilityController.createUri( - request, "/spaces/" + spaceId + "/messages", "r"); - - response.status(201); - response.header("Location", uri.toASCIIString()); - - return new JSONObject() - .put("name", spaceName) - .put("uri", uri) - .put("messages-rwd", messagesUri) - .put("messages-rw", messagesReadWriteUri) - .put("messages-r", messagesReadOnlyUri); - }); - } - - public JSONObject postMessage(Request request, Response response) { - var spaceId = Long.parseLong(request.params(":spaceId")); - var json = new JSONObject(request.body()); - var user = json.getString("author"); - if (!user.matches("[a-zA-Z][a-zA-Z0-9]{0,29}")) { - throw new IllegalArgumentException("invalid username"); - } - if (!user.equals(request.attribute("subject"))) { - throw new IllegalArgumentException( - "author must match authenticated user"); - } - var message = json.getString("message"); - if (message.length() > 1024) { - throw new IllegalArgumentException("message is too long"); - } - - return database.withTransaction(tx -> { - var msgId = database.findUniqueLong( - "SELECT NEXT VALUE FOR msg_id_seq;"); - database.updateUnique( - "INSERT INTO messages(space_id, msg_id, msg_time," + - "author, msg_text) " + - "VALUES(?, ?, current_timestamp, ?, ?)", - spaceId, msgId, user, message); - - response.status(201); - var uri = capabilityController.createUri(request, - "/spaces/" + spaceId + "/messages/" + msgId, "rd"); - var readOnlyUri = capabilityController.createUri(request, - "/spaces/" + spaceId + "/messages/" + msgId, "r"); - - response.header("Location", uri.toASCIIString()); - return new JSONObject() - .put("uri", uri) - .put("read-only", readOnlyUri); - }); - } - - public Message readMessage(Request request, Response response) { - var spaceId = Long.parseLong(request.params(":spaceId")); - var msgId = Long.parseLong(request.params(":msgId")); - - var message = database.findUnique(Message.class, - "SELECT author, msg_time, msg_text " + - "FROM messages WHERE msg_id = ? AND space_id = ?", - msgId, spaceId); - - var linkPattern = Pattern.compile("https?://\\S+"); - var matcher = linkPattern.matcher(message.message); - int start = 0; - while (matcher.find(start)) { - var url = matcher.group(); - var preview = fetchLinkPreview(url); - if (preview != null) { - message.links.add(preview); - } - start = matcher.end(); - } - - response.status(200); - return message; - } - - private final HttpClient httpClient = HttpClient.newHttpClient(); - private final URI linkPreviewService = URI.create( - "http://natter-link-preview-service:4567"); - - private JSONObject fetchLinkPreview(String link) { - var url = linkPreviewService.resolve("/preview?url=" + - URLEncoder.encode(link, StandardCharsets.UTF_8)); - var request = HttpRequest.newBuilder(url) - .GET() - .build(); - try { - var response = httpClient.send(request, - BodyHandlers.ofString()); - if (response.statusCode() == 200) { - return new JSONObject(response.body()); - } - } catch (Exception ignored) { } - return null; - } - - public JSONArray findMessages(Request request, Response response) { - var since = Instant.now().minus(1, ChronoUnit.DAYS); - if (request.queryParams("since") != null) { - since = Instant.parse(request.queryParams("since")); - } - var spaceId = Long.parseLong(request.params(":spaceId")); - - var messages = database.findAll(Long.class, - "SELECT msg_id FROM messages " + - "WHERE space_id = ? AND msg_time >= ?;", - spaceId, since); - - var perms = request.attribute("perms") - .replace("w", ""); - response.status(200); - return new JSONArray(messages.stream() - .map(msgId -> "/spaces/" + spaceId + "/messages/" + msgId) - .map(path -> capabilityController.createUri(request, path, perms)) - .collect(Collectors.toList())); - } - - public JSONObject addMember(Request request, Response response) { - var json = new JSONObject(request.body()); - var spaceId = Long.parseLong(request.params(":spaceId")); - var userToAdd = json.getString("username"); - var roles = json.optJSONArray("roles"); - if (roles == null) { - roles = new JSONArray().put("member"); - } - - if (!DEFINED_ROLES.containsAll(roles.toList())) { - throw new IllegalArgumentException("invalid role"); - } - - for (var role : roles.toList()) { - database.updateUnique( - "INSERT INTO user_roles(space_id, user_id, role_id)" + - " VALUES(?, ?, ?)", spaceId, userToAdd, role); - } - - response.status(200); - return new JSONObject() - .put("username", userToAdd) - .put("roles", roles); - } - - public static class Message { - private final String author; - private final Instant time; - private final String message; - private final List links = new ArrayList<>(); - - public Message(String author, Instant time, String message) { - this.author = author; - this.time = time; - this.message = message; - } - @Override - public String toString() { - JSONObject msg = new JSONObject(); - msg.put("author", author); - msg.put("time", time.toString()); - msg.put("message", message); - msg.put("links", links); - return msg.toString(); - } - } -} \ No newline at end of file diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java deleted file mode 100644 index 73c3678..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/TokenController.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import com.manning.apisecurityinaction.token.*; -import org.json.JSONObject; -import spark.*; - -import java.time.temporal.ChronoUnit; -import java.util.Arrays; - -import static java.time.Instant.now; - -import static spark.Spark.halt; - -public class TokenController { - - private final SecureTokenStore tokenStore; - - public TokenController(SecureTokenStore tokenStore) { - this.tokenStore = tokenStore; - } - - public JSONObject login(Request request, Response response) { - String subject = request.attribute("subject"); - var expiry = now().plus(10, ChronoUnit.MINUTES); - - var token = new TokenStore.Token(expiry, subject); - - var scope = request.queryParams("scope"); - if (scope != null) { - token.attributes.put("scope", scope); - } - - var role = request.queryParams("role"); - if (role != null) { - token.attributes.put("role", role); - } - - var tokenId = tokenStore.create(request, token); - - response.status(201); - return new JSONObject() - .put("token", tokenId); - } - - public void validateToken(Request request, Response response) { - var tokenId = request.headers("X-CSRF-Token"); - if (tokenId == null) { - return; - } - - tokenStore.read(request, tokenId).ifPresent(token -> { - if (now().isBefore(token.expiry)) { - request.attribute("subject", token.username); - token.attributes.forEach(request::attribute); - } else { - response.header("WWW-Authenticate", - "Bearer error=\"invalid_token\"," + - "error_description=\"Expired\""); - halt(401); - } - }); - } - - public Filter requireScope(String method, String requiredScope) { - return (request, response) -> { - if (!method.equals(request.requestMethod())) return; - - var tokenScope = request.attribute("scope"); - if (tokenScope == null) return; - if (!Arrays.asList(tokenScope.split(" ")) - .contains(requiredScope)) { - response.header("WWW-Authenticate", - "Bearer error=\"insufficient_scope\"," + - "scope=\"" + requiredScope + "\""); - halt(403); - } - }; - } - - public JSONObject logout(Request request, Response response) { - var tokenId = request.headers("Authorization"); - if (tokenId == null || !tokenId.startsWith("Bearer ")) { - throw new IllegalArgumentException("missing token header"); - } - tokenId = tokenId.substring(7); - - tokenStore.revoke(request, tokenId); - - response.status(200); - return new JSONObject(); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java b/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java deleted file mode 100644 index 3c2e1e4..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/controller/UserController.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.manning.apisecurityinaction.controller; - -import java.io.*; -import java.net.URLDecoder; -import java.security.cert.*; -import java.util.Base64; - -import com.lambdaworks.crypto.SCryptUtil; -import org.dalesbred.Database; -import org.dalesbred.query.QueryBuilder; -import org.json.JSONObject; -import spark.*; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static spark.Spark.halt; - -public class UserController { - static final String USERNAME_PATTERN = - "[a-zA-Z][a-zA-Z0-9]{1,29}"; - private static final int DNS_TYPE = 2; - - private final Database database; - - public UserController(Database database) { - this.database = database; - } - - public JSONObject registerUser(Request request, - Response response) throws Exception { - var json = new JSONObject(request.body()); - var username = json.getString("username"); - var password = json.optString("password", null); - - if (!username.matches(USERNAME_PATTERN)) { - throw new IllegalArgumentException("invalid username"); - } - - String hash = null; - if (password != null) { - if (password.length() < 8) { - throw new IllegalArgumentException( - "password must be at least 8 characters"); - } - - hash = SCryptUtil.scrypt(password, 32768, 8, 1); - } - database.updateUnique( - "INSERT INTO users(user_id, pw_hash)" + - " VALUES(?, ?)", username, hash); - - response.status(201); - response.header("Location", "/users/" + username); - return new JSONObject().put("username", username); - } - - public void authenticate(Request request, Response response) { - if ("SUCCESS".equals(request.headers("ssl-client-verify"))) { - processClientCertificateAuth(request); - return; - } - var credentials = getCredentials(request); - if (credentials == null) return; - - var username = credentials[0]; - var password = credentials[1]; - - var hash = database.findOptional(String.class, - "SELECT pw_hash FROM users WHERE user_id = ?", username); - - if (hash.isPresent() && SCryptUtil.check(password, hash.get())) { - request.attribute("subject", username); - - var groups = database.findAll(String.class, - "SELECT DISTINCT group_id FROM group_members " + - "WHERE user_id = ?", username); - request.attribute("groups", groups); - } - } - - void processClientCertificateAuth(Request request) { - var pem = request.headers("ssl-client-cert"); - var cert = decodeCert(pem); - try { - if (cert.getSubjectAlternativeNames() == null) { - return; - } - for (var san : cert.getSubjectAlternativeNames()) { - if ((Integer) san.get(0) == DNS_TYPE) { - var subject = (String) san.get(1); - request.attribute("subject", subject); - return; - } - } - } catch (CertificateParsingException e) { - throw new RuntimeException(e); - } - } - - public static X509Certificate decodeCert(String encodedCert) { - var pem = URLDecoder.decode(encodedCert, UTF_8); - try (var in = new ByteArrayInputStream(pem.getBytes(UTF_8))) { - var certFactory = CertificateFactory.getInstance("X.509"); - return (X509Certificate) certFactory.generateCertificate(in); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - String[] getCredentials(Request request) { - var authHeader = request.headers("Authorization"); - if (authHeader == null || !authHeader.startsWith("Basic ")) { - return null; - } - - var offset = "Basic ".length(); - var credentials = new String(Base64.getDecoder().decode( - authHeader.substring(offset)), UTF_8); - - var components = credentials.split(":", 2); - if (components.length != 2) { - throw new IllegalArgumentException("invalid auth header"); - } - - var username = components[0]; - if (!username.matches(USERNAME_PATTERN)) { - throw new IllegalArgumentException("invalid username"); - } - - return components; - } - - public void requireAuthentication(Request request, Response response) { - if (request.attribute("subject") == null) { - response.header("WWW-Authenticate", "Bearer"); - halt(401); - } - } - - public void lookupPermissions(Request request, Response response) { - requireAuthentication(request, response); - - var spaceId = Long.parseLong(request.params(":spaceId")); - var username = (String) request.attribute("subject"); - - var query = new QueryBuilder( - "SELECT rp.perms " + - " FROM role_permissions rp JOIN user_roles ur" + - " ON rp.role_id = ur.role_id" + - " WHERE ur.space_id = ? AND ur.user_id = ?", - spaceId, username); - - var role = (String) request.attribute("role"); - if (role != null) { - query.append(" AND ur.role_id = ?", role); - } - - var perms = String.join("", - database.findAll(String.class, query.build())); - request.attribute("perms", perms); - } - - public Filter requirePermission(String method, String permission) { - return (request, response) -> { - if (!method.equals(request.requestMethod())) { - return; - } - - var perms = request.attribute("perms"); - if (perms == null || !perms.contains(permission)) { - halt(403); - } - }; - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/AuthenticatedTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/AuthenticatedTokenStore.java deleted file mode 100644 index b55f0d0..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/AuthenticatedTokenStore.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.manning.apisecurityinaction.token; - -public interface AuthenticatedTokenStore extends TokenStore { -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/Base64url.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/Base64url.java deleted file mode 100644 index fb2db27..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/Base64url.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import java.util.Base64; - -public class Base64url { - private static final Base64.Encoder encoder = - Base64.getUrlEncoder().withoutPadding(); - private static final Base64.Decoder decoder = - Base64.getUrlDecoder(); - - public static String encode(byte[] data) { - return encoder.encodeToString(data); - } - - public static byte[] decode(String encoded) { - return decoder.decode(encoded); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/ConfidentialTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/ConfidentialTokenStore.java deleted file mode 100644 index 4de3c70..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/ConfidentialTokenStore.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.manning.apisecurityinaction.token; - -public interface ConfidentialTokenStore extends TokenStore { -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java deleted file mode 100644 index 7abcb14..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/CookieTokenStore.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import java.nio.charset.StandardCharsets; -import java.security.*; -import java.util.*; - -import spark.Request; - -public class CookieTokenStore implements SecureTokenStore { - - @Override - public String create(Request request, Token token) { - - var session = request.session(false); - if (session != null) { - session.invalidate(); - } - session = request.session(true); - - session.attribute("username", token.username); - session.attribute("expiry", token.expiry); - session.attribute("attrs", token.attributes); - - return Base64url.encode(sha256(session.id())); - } - - @Override - public Optional read(Request request, String tokenId) { - - var session = request.session(false); - if (session == null) { - return Optional.empty(); - } - - var provided = Base64url.decode(tokenId); - var computed = sha256(session.id()); - - if (!MessageDigest.isEqual(computed, provided)) { - return Optional.empty(); - } - - var token = new Token(session.attribute("expiry"), - session.attribute("username")); - token.attributes.putAll(session.attribute("attrs")); - - return Optional.of(token); - } - - @Override - public void revoke(Request request, String tokenId) { - var session = request.session(false); - if (session == null) return; - - var provided = Base64url.decode(tokenId); - var computed = sha256(session.id()); - - if (!MessageDigest.isEqual(computed, provided)) { - return; - } - - session.invalidate(); - } - - static byte[] sha256(String tokenId) { - try { - var sha256 = MessageDigest.getInstance("SHA-256"); - return sha256.digest( - tokenId.getBytes(StandardCharsets.UTF_8)); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java deleted file mode 100644 index f80dbc6..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/DatabaseTokenStore.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import java.security.SecureRandom; -import java.sql.*; -import java.util.Optional; -import java.util.concurrent.*; - -import org.dalesbred.Database; -import org.json.JSONObject; -import org.slf4j.*; -import spark.Request; - -import static com.manning.apisecurityinaction.token.CookieTokenStore.sha256; - -public class DatabaseTokenStore implements ConfidentialTokenStore { - private static final Logger logger = - LoggerFactory.getLogger(DatabaseTokenStore.class); - - private final Database database; - private final SecureRandom secureRandom; - - public DatabaseTokenStore(Database database) { - this.database = database; - this.secureRandom = new SecureRandom(); - - Executors.newSingleThreadScheduledExecutor() - .scheduleAtFixedRate(this::deleteExpiredTokens, - 10, 10, TimeUnit.MINUTES); - } - - private String randomId() { - var bytes = new byte[20]; - secureRandom.nextBytes(bytes); - return Base64url.encode(bytes); - } - - @Override - public String create(Request request, Token token) { - var tokenId = randomId(); - var attrs = new JSONObject(token.attributes).toString(); - - database.updateUnique("INSERT INTO " + - "tokens(token_id, user_id, expiry, attributes) " + - "VALUES(?, ?, ?, ?)", hash(tokenId), token.username, - token.expiry, attrs); - - return tokenId; - } - - @Override - public Optional read(Request request, String tokenId) { - return database.findOptional(this::readToken, - "SELECT user_id, expiry, attributes " + - "FROM tokens WHERE token_id = ?", hash(tokenId)); - } - - @Override - public void revoke(Request request, String tokenId) { - database.update("DELETE FROM tokens WHERE token_id = ?", - hash(tokenId)); - } - - private String hash(String tokenId) { - var hash = sha256(tokenId); - return Base64url.encode(hash); - } - - private Token readToken(ResultSet resultSet) - throws SQLException { - var username = resultSet.getString(1); - var expiry = resultSet.getTimestamp(2).toInstant(); - var json = new JSONObject(resultSet.getString(3)); - - var token = new Token(expiry, username); - for (var key : json.keySet()) { - token.attributes.put(key, json.getString(key)); - } - return token; - } - - private void deleteExpiredTokens() { - var deleted = database.update( - "DELETE FROM tokens WHERE expiry < current_timestamp"); - logger.info("Deleted {} expired tokens", deleted); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedJwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedJwtTokenStore.java deleted file mode 100644 index c9c64f4..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedJwtTokenStore.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import com.nimbusds.jose.*; -import com.nimbusds.jose.crypto.*; -import com.nimbusds.jwt.*; -import spark.Request; - -import javax.crypto.SecretKey; -import java.text.ParseException; -import java.util.*; - -public class EncryptedJwtTokenStore implements SecureTokenStore { - - private final SecretKey encKey; - private final DatabaseTokenStore tokenWhitelist; - - public EncryptedJwtTokenStore(SecretKey encKey, - DatabaseTokenStore tokenWhitelist) { - this.encKey = encKey; - this.tokenWhitelist = tokenWhitelist; - } - - @Override - public String create(Request request, Token token) { - var whitelistToken = new Token(token.expiry, token.username); - var jwtId = tokenWhitelist.create(request, whitelistToken); - - var claimsBuilder = new JWTClaimsSet.Builder() - .jwtID(jwtId) - .subject(token.username) - .audience("https://localhost:4567") - .expirationTime(Date.from(token.expiry)); - token.attributes.forEach(claimsBuilder::claim); - - var header = new JWEHeader(JWEAlgorithm.DIR, - EncryptionMethod.A128CBC_HS256); - var jwt = new EncryptedJWT(header, claimsBuilder.build()); - - try { - var encrypter = new DirectEncrypter(encKey); - jwt.encrypt(encrypter); - } catch (JOSEException e) { - throw new RuntimeException(e); - } - - return jwt.serialize(); - } - - @Override - public Optional read(Request request, String tokenId) { - try { - var jwt = EncryptedJWT.parse(tokenId); - - var decryptor = new DirectDecrypter(encKey); - jwt.decrypt(decryptor); - - var claims = jwt.getJWTClaimsSet(); - var jwtId = claims.getJWTID(); - if (tokenWhitelist.read(request, jwtId).isEmpty()) { - return Optional.empty(); - } - - if (!claims.getAudience().contains("https://localhost:4567")) { - return Optional.empty(); - } - var expiry = claims.getExpirationTime().toInstant(); - var subject = claims.getSubject(); - - var token = new Token(expiry, subject); - var ignore = Set.of("exp", "sub", "aud"); - for (var attr : claims.getClaims().keySet()) { - if (ignore.contains(attr)) continue; - token.attributes.put(attr, claims.getStringClaim(attr)); - } - - return Optional.of(token); - } catch (ParseException | JOSEException e) { - return Optional.empty(); - } - } - - @Override - public void revoke(Request request, String tokenId) { - try { - var jwt = EncryptedJWT.parse(tokenId); - - var decryptor = new DirectDecrypter(encKey); - jwt.decrypt(decryptor); - var claims = jwt.getJWTClaimsSet(); - - tokenWhitelist.revoke(request, claims.getJWTID()); - } catch (ParseException | JOSEException e) { - throw new IllegalArgumentException("invalid token", e); - } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java deleted file mode 100644 index a34830d..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/EncryptedTokenStore.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import java.security.Key; -import java.util.Optional; - -import software.pando.crypto.nacl.SecretBox; -import spark.Request; - -public class EncryptedTokenStore implements SecureTokenStore { - - private final TokenStore delegate; - private final Key encryptionKey; - - public EncryptedTokenStore(TokenStore delegate, Key encryptionKey) { - this.delegate = delegate; - this.encryptionKey = encryptionKey; - } - - @Override - public String create(Request request, Token token) { - var tokenId = Base64url.decode(delegate.create(request, token)); - return SecretBox.encrypt(encryptionKey, tokenId).toString(); - } - - @Override - public Optional read(Request request, String tokenId) { - var box = SecretBox.fromString(tokenId); - var originalTokenId = Base64url.encode(box.decrypt(encryptionKey)); - return delegate.read(request, originalTokenId); - } - - @Override - public void revoke(Request request, String tokenId) { - var box = SecretBox.fromString(tokenId); - var originalTokenId = Base64url.encode(box.decrypt(encryptionKey)); - delegate.revoke(request, originalTokenId); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java deleted file mode 100644 index 6e7e5e2..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/HmacTokenStore.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import javax.crypto.Mac; -import java.security.*; -import java.util.Optional; - -import spark.Request; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class HmacTokenStore implements SecureTokenStore { - - private final TokenStore delegate; - private final Key macKey; - - private HmacTokenStore(TokenStore delegate, Key macKey) { - this.delegate = delegate; - this.macKey = macKey; - } - public static SecureTokenStore wrap(ConfidentialTokenStore store, - Key macKey) { - return new HmacTokenStore(store, macKey); - } - public static AuthenticatedTokenStore wrap(TokenStore store, - Key macKey) { - return new HmacTokenStore(store, macKey); - } - - @Override - public String create(Request request, Token token) { - var tokenId = delegate.create(request, token); - var tag = hmac(tokenId); - - return tokenId + '.' + Base64url.encode(tag); - } - - private byte[] hmac(String tokenId) { - try { - var mac = Mac.getInstance(macKey.getAlgorithm()); - mac.init(macKey); - return mac.doFinal(tokenId.getBytes(UTF_8)); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } - } - - @Override - public Optional read(Request request, String tokenId) { - var index = tokenId.lastIndexOf('.'); - if (index == -1) return Optional.empty(); - - var realTokenId = tokenId.substring(0, index); - var tag = tokenId.substring(index + 1); - - var provided = Base64url.decode(tag); - var computed = hmac(realTokenId); - - if (!MessageDigest.isEqual(provided, computed)) { - return Optional.empty(); - } - - return delegate.read(request, realTokenId); - } - - @Override - public void revoke(Request request, String tokenId) { - var index = tokenId.lastIndexOf('.'); - if (index == -1) return; - var realTokenId = tokenId.substring(0, index); - - var provided = Base64url.decode(tokenId.substring(index + 1)); - var computed = hmac(realTokenId); - - if (!MessageDigest.isEqual(provided, computed)) { - return; - } - - delegate.revoke(request, realTokenId); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java deleted file mode 100644 index 5222e98..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JsonTokenStore.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import java.time.Instant; -import java.util.*; - -import org.json.*; -import spark.Request; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class JsonTokenStore implements TokenStore { - - @Override - public String create(Request request, Token token) { - var json = new JSONObject(); - json.put("sub", token.username); - json.put("exp", token.expiry.getEpochSecond()); - json.put("aud", List.of("https://localhost:4567")); - json.put("attrs", token.attributes); - - var jsonBytes = json.toString().getBytes(UTF_8); - return Base64url.encode(jsonBytes); - } - - @Override - public Optional read(Request request, String tokenId) { - try { - var decoded = Base64url.decode(tokenId); - var json = new JSONObject(new String(decoded, UTF_8)); - var expiry = Instant.ofEpochSecond(json.getInt("exp")); - var username = json.optString("sub"); - var audience = json.getJSONArray("aud").toList(); - var attrs = json.getJSONObject("attrs"); - - if (!audience.contains("https://localhost:4567")) { - return Optional.empty(); - } - - var token = new Token(expiry, username); - for (var key : attrs.keySet()) { - token.attributes.put(key, attrs.getString(key)); - } - - return Optional.of(token); - } catch (JSONException e) { - return Optional.empty(); - } - } - - @Override - public void revoke(Request request, String tokenId) { - // TODO - } - -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java deleted file mode 100644 index 74996d1..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/JwtHeaderTokenStore.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import java.util.*; - -import org.json.JSONObject; -import spark.Request; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class JwtHeaderTokenStore implements TokenStore { - - private final TokenStore delegate; - private final JSONObject header; - - public JwtHeaderTokenStore(TokenStore delegate, JSONObject header) { - this.delegate = delegate; - this.header = header; - } - - @Override - public String create(Request request, Token token) { - var tokenId = delegate.create(request, token); - var headerBytes = header.toString().getBytes(UTF_8); - return Base64url.encode(headerBytes) + '.' + tokenId; - } - - @Override - public Optional read(Request request, String tokenId) { - var index = tokenId.indexOf('.'); - if (index == -1) return Optional.empty(); - - var encodedHeader = tokenId.substring(0, index); - var realTokenId = tokenId.substring(index + 1); - - var decodedHeader = Base64url.decode(encodedHeader); - var suppliedHeader = new JSONObject( - new String(decodedHeader, UTF_8)); - - for (var expected : this.header.keySet()) { - if (!Objects.equals(this.header.get(expected), - suppliedHeader.get(expected))) { - return Optional.empty(); - } - } - - return delegate.read(request, realTokenId); - } - - @Override - public void revoke(Request request, String tokenId) { - var index = tokenId.indexOf('.'); - if (index == -1) return; - - var realTokenId = tokenId.substring(index + 1); - delegate.revoke(request, realTokenId); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/MacaroonTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/MacaroonTokenStore.java deleted file mode 100644 index c7023ee..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/MacaroonTokenStore.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import java.security.Key; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.Optional; - -import com.github.nitram509.jmacaroons.*; -import com.github.nitram509.jmacaroons.verifier.TimestampCaveatVerifier; -import spark.Request; - -public class MacaroonTokenStore implements SecureTokenStore { - private final TokenStore delegate; - private final Key macKey; - - public MacaroonTokenStore(TokenStore delegate, Key macKey) { - this.delegate = delegate; - this.macKey = macKey; - } - - @Override - public String create(Request request, Token token) { - var identifier = delegate.create(request, token); - var macaroon = MacaroonsBuilder.create("", - macKey.getEncoded(), identifier); - - if (token.expiry != Instant.MAX) { - macaroon = MacaroonsBuilder.modify(macaroon) - .add_first_party_caveat("time < " + token.expiry) - .getMacaroon(); - } - - return macaroon.serialize(); - } - - @Override - public Optional read(Request request, String tokenId) { - var macaroon = MacaroonsBuilder.deserialize(tokenId); - - var verifier = new MacaroonsVerifier(macaroon); - verifier.satisfyGeneral(new TimestampCaveatVerifier()); - verifier.satisfyExact("method = " + request.requestMethod()); - verifier.satisfyGeneral(new SinceVerifier(request)); - - if (verifier.isValid(macKey.getEncoded())) { - return delegate.read(request, macaroon.identifier); - } - return Optional.empty(); - } - - @Override - public void revoke(Request request, String tokenId) { - var macaroon = MacaroonsBuilder.deserialize(tokenId); - delegate.revoke(request, macaroon.identifier); - } - - private static class SinceVerifier implements GeneralCaveatVerifier { - private final Request request; - - private SinceVerifier(Request request) { - this.request = request; - } - - @Override - public boolean verifyCaveat(String caveat) { - if (caveat.startsWith("since > ")) { - var minSince = Instant.parse(caveat.substring(8)); - var reqSince = Instant.now().minus(1, ChronoUnit.DAYS); - if (request.queryParams("since") != null) { - reqSince = Instant.parse(request.queryParams("since")); - } - return reqSince.isAfter(minSince); - } - - return false; - } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java deleted file mode 100644 index 7c92af2..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/OAuth2TokenStore.java +++ /dev/null @@ -1,167 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import javax.net.ssl.*; -import java.io.*; -import java.net.*; -import java.net.http.*; -import java.net.http.HttpRequest.BodyPublishers; -import java.net.http.HttpResponse.BodyHandlers; -import java.security.*; -import java.security.cert.*; -import java.time.Instant; -import java.util.*; - -import com.manning.apisecurityinaction.controller.UserController; -import org.json.JSONObject; -import spark.Request; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class OAuth2TokenStore implements SecureTokenStore { - - private final URI introspectionEndpoint; - private final String authorization; - - private final HttpClient httpClient; - - public OAuth2TokenStore(URI introspectionEndpoint, - String clientId, String clientSecret) { - this.introspectionEndpoint = introspectionEndpoint; - - var credentials = URLEncoder.encode(clientId, UTF_8) + ":" + - URLEncoder.encode(clientSecret, UTF_8); - this.authorization = "Basic " + Base64.getEncoder() - .encodeToString(credentials.getBytes(UTF_8)); - - var sslParams = new SSLParameters(); - sslParams.setProtocols(new String[] { - // TLS 1.3 cipher suites - "TLS_AES_128_GCM_SHA256", - "TLS_AES_256_GCM_SHA384", - "TLS_CHACHA20_POLY1305_SHA256", - - // TLS 1.2 cipher suites - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" - }); - sslParams.setUseCipherSuitesOrder(true); - sslParams.setEndpointIdentificationAlgorithm("HTTPS"); - - try { - var trustedCerts = KeyStore.getInstance("PKCS12"); - trustedCerts.load( - new FileInputStream("as.example.com.ca.p12"), - "changeit".toCharArray()); - var tmf = TrustManagerFactory.getInstance("PKIX"); - tmf.init(trustedCerts); - var sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, tmf.getTrustManagers(), null); - - this.httpClient = HttpClient.newBuilder() - .sslParameters(sslParams) - .sslContext(sslContext) - .build(); - - } catch (GeneralSecurityException | IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public String create(Request request, Token token) { - throw new UnsupportedOperationException(); - } - - @Override - public Optional read(Request request, String tokenId) { - if (!tokenId.matches("[\\x20-\\x7E]{1,1024}")) { - return Optional.empty(); - } - - var form = "token=" + URLEncoder.encode(tokenId, UTF_8) + - "&token_type_hint=access_token"; - - var httpRequest = HttpRequest.newBuilder() - .uri(introspectionEndpoint) - .header("Content-Type", - "application/x-www-form-urlencoded") - .header("Authorization", authorization) - .POST(BodyPublishers.ofString(form)) - .build(); - - try { - var httpResponse = httpClient.send(httpRequest, - BodyHandlers.ofString()); - - if (httpResponse.statusCode() == 200) { - var json = new JSONObject(httpResponse.body()); - - if (json.getBoolean("active")) { - return processResponse(json, request); - } - } - } catch (IOException e) { - throw new RuntimeException(e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - - return Optional.empty(); - } - - private Optional processResponse(JSONObject response, - Request originalRequest) { - var expiry = Instant.ofEpochSecond(response.getLong("exp")); - var subject = response.getString("sub"); - - var confirmationKey = response.optJSONObject("cnf"); - if (confirmationKey != null) { - for (var method : confirmationKey.keySet()) { - if (!"x5t#S256".equals(method)) { - throw new RuntimeException( - "Unknown confirmation method: " + method); - } - if (!"SUCCESS".equals( - originalRequest.headers("ssl-client-verify"))) { - return Optional.empty(); - } - var expectedHash = Base64url.decode( - confirmationKey.getString(method)); - var cert = UserController.decodeCert( - originalRequest.headers("ssl-client-cert")); - var certHash = thumbprint(cert); - if (!MessageDigest.isEqual(expectedHash, certHash)) { - return Optional.empty(); - } - } - } - - var token = new Token(expiry, subject); - - token.attributes.put("scope", response.getString("scope")); - token.attributes.put("client_id", - response.optString("client_id")); - - return Optional.of(token); - } - - - private byte[] thumbprint(X509Certificate certificate) { - try { - var sha256 = MessageDigest.getInstance("SHA-256"); - return sha256.digest(certificate.getEncoded()); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public void revoke(Request request, String tokenId) { - throw new UnsupportedOperationException(); - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/RemoteTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/RemoteTokenStore.java deleted file mode 100644 index 69d2822..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/RemoteTokenStore.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import java.io.IOException; -import java.net.URI; -import java.net.http.*; -import java.net.http.HttpRequest.BodyPublishers; -import java.net.http.HttpResponse.BodyHandlers; -import java.util.Optional; - -import org.json.JSONObject; -import spark.Request; - -public class RemoteTokenStore implements SecureTokenStore { - - private final URI tokenServiceUri; - private final HttpClient httpClient; - - public RemoteTokenStore(String tokenServiceUri) { - this.tokenServiceUri = URI.create(tokenServiceUri); - this.httpClient = HttpClient.newBuilder().build(); - } - - @Override - public String create(Request request, Token token) { - var json = token.toJson(); - var httpRequest = HttpRequest.newBuilder(tokenServiceUri) - .POST(BodyPublishers.ofString(json.toString())) - .build(); - - return send(httpRequest).getString("tokenId"); - } - - @Override - public Optional read(Request request, String tokenId) { - var httpRequest = HttpRequest.newBuilder() - .uri(tokenServiceUri.resolve("/tokens/" + tokenId)) - .GET() - .build(); - - try { - var tokenJson = send(httpRequest); - var token = Token.fromJson(tokenJson); - return Optional.of(token); - } catch (RuntimeException e) { - return Optional.empty(); - } - } - - @Override - public void revoke(Request request, String tokenId) { - var httpRequest = HttpRequest.newBuilder() - .uri(tokenServiceUri.resolve("/tokens/" + tokenId)) - .DELETE() - .build(); - - send(httpRequest); - } - - private JSONObject send(HttpRequest request) { - try { - var response = httpClient.send(request, - BodyHandlers.ofString()); - if (response.statusCode() / 100 != 2) { - throw new RuntimeException( - "Bad response from token service: " - + response.statusCode()); - } - return new JSONObject(response.body()); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SecureTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SecureTokenStore.java deleted file mode 100644 index c4d6736..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SecureTokenStore.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.manning.apisecurityinaction.token; - -public interface SecureTokenStore extends ConfidentialTokenStore, - AuthenticatedTokenStore { -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java deleted file mode 100644 index 2cedf0b..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtAccessTokenStore.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import java.net.*; -import java.text.ParseException; -import java.util.Optional; - -import com.nimbusds.jose.*; -import com.nimbusds.jose.jwk.source.*; -import com.nimbusds.jose.proc.*; -import com.nimbusds.jwt.proc.DefaultJWTProcessor; -import spark.Request; - -public class SignedJwtAccessTokenStore implements SecureTokenStore { - - private final String expectedIssuer; - private final String expectedAudience; - private final JWSAlgorithm signatureAlgorithm; - private final JWKSource jwkSource; - - public SignedJwtAccessTokenStore(String expectedIssuer, - String expectedAudience, - JWSAlgorithm signatureAlgorithm, - URI jwkSetUri) - throws MalformedURLException { - this.expectedIssuer = expectedIssuer; - this.expectedAudience = expectedAudience; - this.signatureAlgorithm = signatureAlgorithm; - this.jwkSource = new RemoteJWKSet<>(jwkSetUri.toURL()); - } - - @Override - public String create(Request request, Token token) { - throw new UnsupportedOperationException(); - } - - @Override - public void revoke(Request request, String tokenId) { - throw new UnsupportedOperationException(); - } - - @Override - public Optional read(Request request, String tokenId) { - try { - var verifier = new DefaultJWTProcessor<>(); - var keySelector = new JWSVerificationKeySelector<>( - signatureAlgorithm, jwkSource); - verifier.setJWSKeySelector(keySelector); - - var claims = verifier.process(tokenId, null); - - if (!expectedIssuer.equals(claims.getIssuer())) { - return Optional.empty(); - } - if (!claims.getAudience().contains(expectedAudience)) { - return Optional.empty(); - } - - var expiry = claims.getExpirationTime().toInstant(); - var subject = claims.getSubject(); - var token = new Token(expiry, subject); - - String scope; - try { - scope = claims.getStringClaim("scope"); - } catch (ParseException e) { - scope = String.join(" ", - claims.getStringListClaim("scope")); - } - token.attributes.put("scope", scope); - return Optional.of(token); - - } catch (ParseException | BadJOSEException | JOSEException e) { - return Optional.empty(); - } - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java deleted file mode 100644 index 2462021..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/SignedJwtTokenStore.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import java.text.ParseException; -import java.util.*; - -import com.nimbusds.jose.*; -import com.nimbusds.jwt.*; -import spark.Request; - -public class SignedJwtTokenStore implements AuthenticatedTokenStore { - private final JWSSigner signer; - private final JWSVerifier verifier; - private final JWSAlgorithm algorithm; - private final String audience; - - public SignedJwtTokenStore(JWSSigner signer, - JWSVerifier verifier, JWSAlgorithm algorithm, - String audience) { - this.signer = signer; - this.verifier = verifier; - this.algorithm = algorithm; - this.audience = audience; - } - - @Override - public String create(Request request, Token token) { - var claimsSet = new JWTClaimsSet.Builder() - .subject(token.username) - .audience(audience) - .expirationTime(Date.from(token.expiry)) - .claim("attrs", token.attributes) - .build(); - var header = new JWSHeader(algorithm); - var jwt = new SignedJWT(header, claimsSet); - try { - jwt.sign(signer); - return jwt.serialize(); - } catch (JOSEException e) { - throw new RuntimeException(e); - } - } - - @Override - public Optional read(Request request, String tokenId) { - try { - var jwt = SignedJWT.parse(tokenId); - if (!jwt.verify(verifier)) { - throw new JOSEException("Invalid signature"); - } - - var claims = jwt.getJWTClaimsSet(); - if (!claims.getAudience().contains(audience)) { - throw new JOSEException("Incorrect audience"); - } - - var expiry = claims.getExpirationTime().toInstant(); - var subject = claims.getSubject(); - var token = new Token(expiry, subject); - var attrs = claims.getJSONObjectClaim("attrs"); - attrs.forEach((key, value) -> - token.attributes.put(key, (String) value)); - - return Optional.of(token); - } catch (ParseException | JOSEException e) { - return Optional.empty(); - } - } - - @Override - public void revoke(Request request, String tokenId) { - // TODO - } -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java deleted file mode 100644 index 2110af1..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/TokenStore.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import org.json.JSONObject; -import spark.Request; - -import java.time.Instant; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -public interface TokenStore { - - String create(Request request, Token token); - Optional read(Request request, String tokenId); - void revoke(Request request, String tokenId); - - class Token { - public final Instant expiry; - public final String username; - public final Map attributes; - - public Token(Instant expiry, String username) { - this.expiry = expiry; - this.username = username; - this.attributes = new ConcurrentHashMap<>(); - } - - public JSONObject toJson() { - return new JSONObject() - .put("exp", expiry.getEpochSecond()) - .put("sub", username) - .put("attrs", attributes); - } - - public static Token fromJson(JSONObject json) { - var expiry = Instant.ofEpochSecond(json.getLong("exp")); - var user = json.optString("sub"); - var attrs = new LinkedHashMap(); - json.getJSONObject("attrs").toMap() - .forEach((key, value) -> attrs.put(key, value.toString())); - - var token = new Token(expiry, user); - token.attributes.putAll(attrs); - return token; - } - } - -} diff --git a/natter-api/src/main/java/com/manning/apisecurityinaction/token/UnauthenticatedEncryptionStore.java b/natter-api/src/main/java/com/manning/apisecurityinaction/token/UnauthenticatedEncryptionStore.java deleted file mode 100644 index 0218df5..0000000 --- a/natter-api/src/main/java/com/manning/apisecurityinaction/token/UnauthenticatedEncryptionStore.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.manning.apisecurityinaction.token; - -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import java.security.*; -import java.util.Optional; - -import spark.Request; - -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * This token store encrypts the contents of the token using AES in - * unauthenticated counter mode. This is provided purely as an example - * of using types to enforce security properties. You should use the - * {@link EncryptedTokenStore} or {@link EncryptedJwtTokenStore} - * instead of this store. - */ -public class UnauthenticatedEncryptionStore implements ConfidentialTokenStore { - - private final Key encKey; - private final TokenStore delegate; - - public UnauthenticatedEncryptionStore(Key encKey, TokenStore delegate) { - this.encKey = encKey; - this.delegate = delegate; - } - - @Override - public String create(Request request, Token token) { - var tokenId = delegate.create(request, token); - return encrypt(tokenId.getBytes(UTF_8)); - } - - @Override - public Optional read(Request request, String tokenId) { - return decrypt(tokenId).flatMap(tok -> delegate.read(request, tok)); - } - - @Override - public void revoke(Request request, String tokenId) { - decrypt(tokenId).ifPresent(tok -> delegate.revoke(request, tok)); - } - - private String encrypt(byte[] data) { - try { - var cipher = Cipher.getInstance("AES/CTR/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, encKey); - var ciphertext = cipher.doFinal(data); - var iv = cipher.getIV(); - return Base64url.encode(iv) + '.' + Base64url.encode(ciphertext); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } - } - - private Optional decrypt(String encrypted) { - var index = encrypted.indexOf('.'); - if (index == -1) return Optional.empty(); - var iv = Base64url.decode(encrypted.substring(0, index)); - var ciphertext = Base64url.decode(encrypted.substring(index + 1)); - try { - var cipher = Cipher.getInstance("AES/CTR/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, encKey, - new IvParameterSpec(iv)); - var plaintext = cipher.doFinal(ciphertext); - return Optional.of(new String(plaintext, UTF_8)); - } catch (GeneralSecurityException e) { - return Optional.empty(); - } - } -} diff --git a/natter-api/src/main/jib/keystore.p12 b/natter-api/src/main/jib/keystore.p12 deleted file mode 100644 index 9007e6c11b68583815fceb513feab72452eed153..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 755 zcmXqLVtUWS$ZXKWw1SOOtIebBJ1-+U;0&a13u_6aG9e-C<__kgM3O2M8; zCT51i0W21FJ07k7#=#mCzP`_tsUzjX$389Ryw(Rl9-o&uJU9ICp83Vs_VPs7?X}vG zw&zhu)Pz=@s5QQ;(heB97|6q&$thweBqzX-!H~<4$dJsS%aF~G%1~*bh)^JED8eEX zl9`*TU}$P;VP<7sm57u8%)D%}v?&+qy&Nr)^IUiJj@8==WaY*v!z>($c`rK+%Ac zja8eEnMsP3fkk9}?c<*8M9&`uC0d&26i(}H_$b-LB2w4MRkB?3?XwqAKTmFyRh9iU MH#m!lnXzpF015*182|tP diff --git a/natter-api/src/main/jib/localhost.p12 b/natter-api/src/main/jib/localhost.p12 deleted file mode 100644 index 3a8322f5305cbb79f7aed716437ebc6451479ede..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3975 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 diff --git a/natter-api/src/main/resources/META-INF/kmodule.xml b/natter-api/src/main/resources/META-INF/kmodule.xml deleted file mode 100644 index ebb4ba4..0000000 --- a/natter-api/src/main/resources/META-INF/kmodule.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/natter-api/src/main/resources/accessrules.drl b/natter-api/src/main/resources/accessrules.drl deleted file mode 100644 index efbb5db..0000000 --- a/natter-api/src/main/resources/accessrules.drl +++ /dev/null @@ -1,15 +0,0 @@ -package com.manning.apisecurityinaction.rules; - -import com.manning.apisecurityinaction.controller.DroolsAccessController.*; -import com.manning.apisecurityinaction.controller.ABACAccessController.Decision; - -global Decision decision; - -rule "deny moderation outside office hours" - when - Action( this["method"] == "DELETE" ) - Environment( this["timeOfDay"].hour < 9 - || this["timeOfDay"].hour > 17 ) - then - decision.deny(); -end \ No newline at end of file diff --git a/natter-api/src/main/resources/public/capability.html b/natter-api/src/main/resources/public/capability.html deleted file mode 100644 index fe3822d..0000000 --- a/natter-api/src/main/resources/public/capability.html +++ /dev/null @@ -1,17 +0,0 @@ - - - Capability test page - - - - -

Natter

- - Load messages - -
- -
AuthorTimeMessage
- - \ No newline at end of file diff --git a/natter-api/src/main/resources/public/capability.js b/natter-api/src/main/resources/public/capability.js deleted file mode 100644 index ed19e9d..0000000 --- a/natter-api/src/main/resources/public/capability.js +++ /dev/null @@ -1,36 +0,0 @@ -function getCap(url, callback) { - let capUrl = new URL(url); - let token = capUrl.password; - capUrl.username = ''; - capUrl.password = ''; - - return fetch(capUrl.href, { - headers: { - 'Authorization': 'Bearer ' + token, - 'X-CSRF-Token': localStorage.getItem('token') - } - }) - .then(response => response.json()) - .then(callback) - .catch(err => console.error('Error: ', err)); -} -function loadMessages(link) { - getCap(link.href, async messages => { - for (let messageUrl of messages) { - await loadMessage(messageUrl); - } - }); - return false; -} -function loadMessage(capUrl) { - return getCap(capUrl, message => { - let table = document.getElementById('messages'); - let row = table.appendChild(document.createElement('tr')); - row.appendChild(document.createElement('td')) - .textContent = message.author; - row.appendChild(document.createElement('td')) - .textContent = message.time; - row.appendChild(document.createElement('td')) - .textContent = message.message; - }); -} diff --git a/natter-api/src/main/resources/public/login.html b/natter-api/src/main/resources/public/login.html deleted file mode 100644 index 5b507ff..0000000 --- a/natter-api/src/main/resources/public/login.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - Natter! - - - - -

Login

-
- - - -
- - diff --git a/natter-api/src/main/resources/public/login.js b/natter-api/src/main/resources/public/login.js deleted file mode 100644 index 021aca6..0000000 --- a/natter-api/src/main/resources/public/login.js +++ /dev/null @@ -1,37 +0,0 @@ -const apiUrl = 'https://localhost:4567'; - -function login(username, password) { - let credentials = 'Basic ' + btoa(username + ':' + password); - - fetch(apiUrl + '/sessions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': credentials - } - }) - .then(res => { - if (res.ok) { - res.json().then(json => { - localStorage.setItem('token', json.token); - window.location.replace('/capability.html'); - }); - } - }) - .catch(error => console.error('Error logging in: ', error)); -} - -window.addEventListener('load', function(e) { - document.getElementById('login') - .addEventListener('submit', processLoginSubmit); -}); - -function processLoginSubmit(e) { - e.preventDefault(); - - let username = document.getElementById('username').value; - let password = document.getElementById('password').value; - - login(username, password); - return false; -} diff --git a/natter-api/src/main/resources/public/natter.html b/natter-api/src/main/resources/public/natter.html deleted file mode 100644 index ec99756..0000000 --- a/natter-api/src/main/resources/public/natter.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - Natter! - - - - -

Create Space

-
- - - -
- - diff --git a/natter-api/src/main/resources/public/natter.js b/natter-api/src/main/resources/public/natter.js deleted file mode 100644 index a94a987..0000000 --- a/natter-api/src/main/resources/public/natter.js +++ /dev/null @@ -1,42 +0,0 @@ -const apiUrl = 'https://localhost:4567'; - -function createSpace(name, owner) { - let data = {name: name, owner: owner}; - let token = localStorage.getItem('token'); - - fetch(apiUrl + '/spaces', { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + token - } - }) - .then(response => { - if (response.ok) { - return response.json(); - } else if (response.status === 401) { - window.location.replace('/login.html'); - } else { - throw Error(response.statusText); - } - }) - .then(json => console.log('Created space: ', json.name, json.uri)) - .catch(error => console.error('Error: ', error)); -} - -window.addEventListener('load', function(e) { - document.getElementById('createSpace') - .addEventListener('submit', processFormSubmit); -}); - -function processFormSubmit(e) { - e.preventDefault(); - - let spaceName = document.getElementById('spaceName').value; - let owner = document.getElementById('owner').value; - - createSpace(spaceName, owner); - - return false; -} diff --git a/natter-api/src/main/resources/schema.sql b/natter-api/src/main/resources/schema.sql deleted file mode 100644 index 658154a..0000000 --- a/natter-api/src/main/resources/schema.sql +++ /dev/null @@ -1,71 +0,0 @@ -CREATE TABLE users( - user_id VARCHAR(30) PRIMARY KEY, - pw_hash VARCHAR(255) -); -CREATE TABLE group_members( - group_id VARCHAR(30), - user_id VARCHAR(30) REFERENCES users(user_id) -); -CREATE INDEX group_member_user_idx ON group_members(user_id); - -CREATE TABLE spaces( - space_id INT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - owner VARCHAR(30) NOT NULL -); -CREATE SEQUENCE space_id_seq; -CREATE TABLE messages( - space_id INT NOT NULL REFERENCES spaces(space_id), - msg_id INT PRIMARY KEY, - author VARCHAR(30) NOT NULL, - msg_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - msg_text VARCHAR(1024) NOT NULL -); -CREATE SEQUENCE msg_id_seq; -CREATE INDEX msg_timestamp_idx ON messages(msg_time); -CREATE UNIQUE INDEX space_name_idx ON spaces(name); - -CREATE TABLE audit_log( - audit_id INT NULL, - method VARCHAR(10) NOT NULL, - path VARCHAR(100) NOT NULL, - user_id VARCHAR(30) NULL, - status INT NULL, - audit_time TIMESTAMP NOT NULL -); -CREATE SEQUENCE audit_id_seq; - -CREATE TABLE role_permissions( - role_id VARCHAR(30) NOT NULL PRIMARY KEY, - perms VARCHAR(3) NOT NULL -); -INSERT INTO role_permissions(role_id, perms) - VALUES ('owner', 'rwd'), - ('moderator', 'rd'), - ('member', 'rw'), - ('observer', 'r'); - -CREATE TABLE user_roles( - space_id INT NOT NULL REFERENCES spaces(space_id), - user_id VARCHAR(30) NOT NULL REFERENCES users(user_id), - role_id VARCHAR(30) NOT NULL REFERENCES role_permissions(role_id) -); -CREATE INDEX user_roles_idx ON user_roles(space_id, user_id); - -CREATE TABLE tokens( - token_id VARCHAR(100) PRIMARY KEY, - user_id VARCHAR(30), - expiry TIMESTAMP NOT NULL, - attributes VARCHAR(4096) NOT NULL -); -CREATE INDEX expired_token_idx ON tokens(expiry); - -CREATE USER natter_api_user PASSWORD 'password'; -GRANT SELECT, INSERT ON spaces, messages TO natter_api_user; -GRANT DELETE ON messages TO natter_api_user; -GRANT SELECT, INSERT ON users TO natter_api_user; -GRANT SELECT, INSERT ON audit_log TO natter_api_user; -GRANT SELECT, INSERT, DELETE ON tokens TO natter_api_user; -GRANT SELECT, INSERT, DELETE ON group_members TO natter_api_user; -GRANT SELECT, INSERT, DELETE ON user_roles TO natter_api_user; -GRANT SELECT ON role_permissions TO natter_api_user; \ No newline at end of file diff --git a/natter-api/test.txt b/natter-api/test.txt deleted file mode 100644 index 38e0270..0000000 --- a/natter-api/test.txt +++ /dev/null @@ -1,26 +0,0 @@ -Lorem ipsum dolor sit amet consectetur adipiscing elit ultrices platea, dapibus eleifend dictum facilisis aliquam -egestas pretium vehicula, euismod metus mauris sociis justo aptent congue fames. Auctor tellus nunc at cursus morbi -viverra velit nisl purus, a malesuada scelerisque sem lectus augue etiam ornare magnis donec, non elementum facilisis in -penatibus aliquet nulla per. Placerat posuere mauris dictumst dis volutpat eleifend natoque nascetur, iaculis curabitur -velit nam sociis erat tristique lobortis, sapien convallis ridiculus magna lacinia cras torquent. Proin pretium commodo -torquent scelerisque conubia etiam eu, nec integer erat ante turpis id et, lobortis rhoncus varius fringilla ad -parturient. Purus id in a tristique nostra vitae fames, bibendum consequat porta montes magna vestibulum ullamcorper -neque, nascetur tincidunt habitant fusce hac risus. In mi aptent eros euismod odio dictumst diam, orci vulputate -parturient cursus hendrerit lobortis ante sodales, litora vivamus cum felis convallis pharetra. Etiam dis duis vel -vulputate scelerisque lacus sem platea praesent primis fermentum vehicula, egestas nec convallis facilisis dictumst -ridiculus inceptos et venenatis penatibus. Hendrerit magnis senectus metus aliquet volutpat curae dapibus mollis posuere -leo, sem blandit nullam tincidunt condimentum sociis tortor enim porta imperdiet, lectus mattis class vehicula facilisi -bibendum vivamus penatibus vestibulum. Porttitor justo suscipit pharetra nam potenti lobortis tempus, hac molestie morbi -sociosqu per vel penatibus, curabitur primis viverra tincidunt nisi nec. Lacinia pellentesque arcu per imperdiet duis -bibendum aliquam tortor risus varius placerat massa, ultricies cubilia nullam cras quam tristique laoreet et metus -integer. Phasellus suspendisse taciti duis blandit montes nam magna class per, ante senectus praesent pulvinar vel ut -fringilla ad, aenean nulla tristique mattis molestie nunc habitasse vitae. Purus porta ornare scelerisque aptent quam -ullamcorper nec lectus at gravida dis, sem rhoncus eleifend tempor est parturient iaculis curabitur vivamus dictumst. -Dignissim nullam condimentum tristique leo lobortis ornare tortor congue class non, mattis velit interdum primis -ultrices fringilla donec vivamus facilisis purus habitant, sed fames aliquet cum netus justo cras placerat dis. Enim ad -fringilla est sed vehicula in nam dignissim, malesuada aenean curabitur ut montes lobortis augue dictumst cursus, metus -neque mus gravida maecenas tristique ligula. Aptent quis blandit tincidunt posuere nisi sem, malesuada nunc viverra -bibendum montes venenatis cras, suscipit feugiat pulvinar nibh mi. Placerat facilisi in class eros diam erat metus -egestas, volutpat natoque aenean ad sapien facilisis ac eget pretium, velit tincidunt ligula porttitor tempus lacus -conubia. Quam suspendisse condimentum pretium lectus facilisis curae accumsan tincidunt, duis torquent cursus blandit -pellentesque fringilla luctus cum mauris, nam aenean sed interdum di. diff --git a/natter-api/xss.html b/natter-api/xss.html deleted file mode 100644 index f56c677..0000000 --- a/natter-api/xss.html +++ /dev/null @@ -1,13 +0,0 @@ - - - -
- -
- - - \ No newline at end of file From 447e05777a8614d7af814bb43b9cefb0bedebc78 Mon Sep 17 00:00:00 2001 From: Neil Madden Date: Fri, 20 Nov 2020 22:15:46 +0000 Subject: [PATCH 209/209] Update README for publication --- README.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5fe12f7..4d8f187 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # API Security in Action -This repository contains source code to accompany the upcoming book -API Security in Action, written by Neil Madden and to be published by -Manning Publications in October 2020. If you have stumbled across -this repository by accident, it is unlikely to make much sense on its -own at this stage. Please see [Manning's website](https://www.manning.com/books/api-security-in-action?a_aid=api_security_in_action&a_bid=6806e3b6) -for early access. +This repository contains source code that accompanies the book +*API Security in Action*, written by Neil Madden and published by +Manning Publications in November 2020. +Please see [Manning's website](https://www.manning.com/books/api-security-in-action?a_aid=api_security_in_action&a_bid=6806e3b6) +for information on purchasing a copy, or its available from Amazon +and other retailers. **Note: there is no source code on the main branch.** You need to check out the branch for the chapter you are reading. @@ -19,8 +19,9 @@ final source code after all the alterations in that chapter. Typically the source code at the end of a chapter is also identical to the start of the next chapter. -The source code can also be downloaded as a zip file from the early -access website. +**I strongly recommend working through the code listings from the book.** + +The source code can also be downloaded as a zip file from Manning's website. ## Prerequisites @@ -44,6 +45,12 @@ descriptions for HTTP requests that can be used. Chapter 10 and onwards have more detailed requirements to run the sample code. Please consult the book for exact instructions. +## Postman + +I've created a [Postman](https://www.postman.com) collection to help you perform operations using the API developed +during the book as an alternative to curl. You can import the collection from this url: +https://www.postman.com/collections/ef49c7f5cba0737ecdfd + ## Chapters ### Chapter 2 - Secure API development