├── as ├── system.properties ├── src │ └── main │ │ ├── resources │ │ ├── application-heroku.yml │ │ ├── application.yml │ │ └── static │ │ │ └── index.html │ │ ├── java │ │ └── io │ │ │ └── bspk │ │ │ └── oauth │ │ │ └── xyz │ │ │ ├── authserver │ │ │ ├── repository │ │ │ │ ├── ClientRepository.java │ │ │ │ └── TransactionRepository.java │ │ │ ├── data │ │ │ │ └── api │ │ │ │ │ ├── ApprovalRequest.java │ │ │ │ │ ├── UserInteractionFormSubmission.java │ │ │ │ │ ├── ApprovalResponse.java │ │ │ │ │ └── PendingApproval.java │ │ │ ├── api │ │ │ │ ├── DiscoveryController.java │ │ │ │ └── TransactionApi.java │ │ │ └── endpoint │ │ │ │ └── InteractionEndpoint.java │ │ │ └── Application.java │ │ └── js │ │ ├── api │ │ ├── uriTemplateInterceptor.js │ │ └── uriListConverter.js │ │ ├── http.js │ │ ├── app.js │ │ ├── authserver.js │ │ └── interact.js ├── package.json ├── webpack.config.js └── pom.xml ├── rc ├── system.properties ├── src │ ├── main │ │ ├── js │ │ │ ├── db.js │ │ │ ├── api │ │ │ │ ├── uriTemplateInterceptor.js │ │ │ │ └── uriListConverter.js │ │ │ ├── app.js │ │ │ └── http.js │ │ ├── resources │ │ │ ├── application-heroku.yml │ │ │ ├── application.yml │ │ │ └── static │ │ │ │ └── index.html │ │ └── java │ │ │ └── io │ │ │ └── bspk │ │ │ └── oauth │ │ │ └── xyz │ │ │ ├── client │ │ │ ├── repository │ │ │ │ └── PendingTransactionRepository.java │ │ │ └── api │ │ │ │ └── DiscoveryController.java │ │ │ ├── data │ │ │ ├── api │ │ │ │ └── ClientApiRequest.java │ │ │ └── PendingTransaction.java │ │ │ ├── Application.java │ │ │ └── http │ │ │ └── SigningRestTemplateService.java │ └── test │ │ └── java │ │ └── io │ │ └── bspk │ │ └── oauth │ │ └── xyz │ │ └── ApplicationTests.java ├── package.json ├── webpack.config.js └── pom.xml ├── rs ├── system.properties ├── src │ └── main │ │ ├── js │ │ ├── db.js │ │ ├── api │ │ │ ├── uriTemplateInterceptor.js │ │ │ └── uriListConverter.js │ │ ├── http.js │ │ └── app.js │ │ ├── resources │ │ ├── application-heroku.yml │ │ ├── application.yml │ │ └── static │ │ │ └── index.html │ │ └── java │ │ └── io │ │ └── bspk │ │ └── oauth │ │ └── xyz │ │ ├── rs │ │ ├── TokenRepository.java │ │ ├── DiscoveryController.java │ │ └── ResourceEndpoint.java │ │ └── Application.java ├── package.json ├── webpack.config.js └── pom.xml ├── lib ├── package-lock.json ├── src │ └── main │ │ └── java │ │ └── io │ │ └── bspk │ │ └── oauth │ │ └── xyz │ │ ├── data │ │ ├── Resource.java │ │ ├── api │ │ │ ├── PushbackRequest.java │ │ │ ├── TransactionContinueRequest.java │ │ │ ├── DisplayRequest.java │ │ │ ├── ErrorCode.java │ │ │ ├── InteractHintRequest.java │ │ │ ├── ContinueResponse.java │ │ │ ├── UserRequest.java │ │ │ ├── UserCodeResponse.java │ │ │ ├── UserCodeUriResponse.java │ │ │ ├── InteractRequest.java │ │ │ ├── InteractResponse.java │ │ │ ├── ClientRequest.java │ │ │ ├── RequestedResource.java │ │ │ ├── SubjectRequest.java │ │ │ ├── KeyRequest.java │ │ │ ├── AccessTokenRequest.java │ │ │ ├── AccessTokenResponse.java │ │ │ ├── HandleAwareField.java │ │ │ ├── TransactionRequest.java │ │ │ ├── MultipleAwareField.java │ │ │ └── TransactionResponse.java │ │ ├── Client.java │ │ ├── Display.java │ │ ├── Assertion.java │ │ ├── User.java │ │ ├── InteractFinish.java │ │ ├── AccessToken.java │ │ ├── Interact.java │ │ ├── Key.java │ │ ├── Subject.java │ │ └── Transaction.java │ │ ├── http │ │ ├── StructuredDictionaryConverter.java │ │ ├── DigestWrappingFilter.java │ │ └── JoseUnwrappingFilter.java │ │ ├── crypto │ │ ├── KeyProofParameters.java │ │ └── Hash.java │ │ └── json │ │ ├── SubjectIdentifierFormatSerializer.java │ │ ├── HandleAwareFieldSerializer.java │ │ ├── SubjectIdentifierFormatDeserializer.java │ │ ├── MultipleAwareFieldSerializer.java │ │ ├── JWKSerializer.java │ │ ├── JWTSerializer.java │ │ ├── JWKDeserializer.java │ │ ├── JWTDeserializer.java │ │ ├── HandleAwareFieldDeserializer.java │ │ └── MultipleAwareFieldDeserializer.java └── pom.xml ├── docker-compose.yml ├── .gitignore ├── README.md └── LICENSE.md /as/system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=15 2 | -------------------------------------------------------------------------------- /rc/system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=15 2 | -------------------------------------------------------------------------------- /rs/system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=15 2 | -------------------------------------------------------------------------------- /lib/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /rc/src/main/js/db.js: -------------------------------------------------------------------------------- 1 | import Dexie from 'dexie'; 2 | 3 | const db = new Dexie('xyzspa'); 4 | db.version(1).stores({ savedState: '++id' }); 5 | 6 | export default db; -------------------------------------------------------------------------------- /rs/src/main/js/db.js: -------------------------------------------------------------------------------- 1 | import Dexie from 'dexie'; 2 | 3 | const db = new Dexie('xyzspa'); 4 | db.version(1).stores({ savedState: '++id' }); 5 | 6 | export default db; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mongodb: 4 | image: mongo 5 | volumes: 6 | - ./mongo/data:/data/db:delegated 7 | ports: 8 | - "27017:27017" 9 | -------------------------------------------------------------------------------- /as/src/main/resources/application-heroku.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data.mongodb.uri: 3 | mongodb+srv://oauth_xyz_as:${MONGO_PW}@cluster0.39rin.mongodb.net/oauth_xyz_as?retryWrites=true&w=majority 4 | 5 | oauth.xyz.root: https://gnap-as.herokuapp.com 6 | -------------------------------------------------------------------------------- /rs/src/main/resources/application-heroku.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data.mongodb.uri: ## This is the same data store the AS uses, so that the RS can look up token information directly 3 | mongodb+srv://oauth_xyz_as:${MONGO_PW}@cluster0.39rin.mongodb.net/oauth_xyz_as?retryWrites=true&w=majority 4 | 5 | oauth.xyz.root: https://gnap-rs.herokuapp.com/ 6 | -------------------------------------------------------------------------------- /rc/src/main/resources/application-heroku.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data.mongodb.uri: 3 | mongodb+srv://oauth_xyz_c:${MONGO_PW}@cluster0.39rin.mongodb.net/oauth_xyz_c?retryWrites=true&w=majority 4 | 5 | oauth.xyz: 6 | root: https://gnap-c.herokuapp.com 7 | asEndpoint: https://gnap-as.herokuapp.com/api/as/transaction 8 | rsEndpoint: https://gnap-rs.herokuapp.com/api/rs 9 | -------------------------------------------------------------------------------- /rs/src/main/java/io/bspk/oauth/xyz/rs/TokenRepository.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.rs; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | import io.bspk.oauth.xyz.data.Transaction; 6 | 7 | /** 8 | * @author jricher 9 | * 10 | */ 11 | public interface TokenRepository extends CrudRepository { 12 | 13 | Transaction findFirstByAccessTokenDataValue(String value); 14 | 15 | } -------------------------------------------------------------------------------- /rc/src/test/java/io/bspk/oauth/xyz/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | build/ 22 | nbbuild/ 23 | dist/ 24 | nbdist/ 25 | .nb-gradle/ 26 | 27 | node_modules/ 28 | /mongo/data/ 29 | 30 | */src/main/resources/static/built 31 | -------------------------------------------------------------------------------- /as/src/main/java/io/bspk/oauth/xyz/authserver/repository/ClientRepository.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.authserver.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | import io.bspk.oauth.xyz.data.Client; 6 | import io.bspk.oauth.xyz.data.Key; 7 | 8 | /** 9 | * @author jricher 10 | * 11 | */ 12 | public interface ClientRepository extends CrudRepository { 13 | 14 | Client findFirstByKey(Key key); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/Resource.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | 6 | import lombok.Data; 7 | import lombok.experimental.Accessors; 8 | 9 | /** 10 | * @author jricher 11 | * 12 | */ 13 | @Data 14 | @Accessors(chain = true) 15 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 16 | public class Resource { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /as/src/main/js/api/uriTemplateInterceptor.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | const interceptor = require('rest/interceptor'); 5 | 6 | return interceptor({ 7 | request: function (request /*, config, meta */) { 8 | /* If the URI is a URI Template per RFC 6570 (http://tools.ietf.org/html/rfc6570), trim out the template part */ 9 | if (request.path.indexOf('{') === -1) { 10 | return request; 11 | } else { 12 | request.path = request.path.split('{')[0]; 13 | return request; 14 | } 15 | } 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /rc/src/main/js/api/uriTemplateInterceptor.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | const interceptor = require('rest/interceptor'); 5 | 6 | return interceptor({ 7 | request: function (request /*, config, meta */) { 8 | /* If the URI is a URI Template per RFC 6570 (http://tools.ietf.org/html/rfc6570), trim out the template part */ 9 | if (request.path.indexOf('{') === -1) { 10 | return request; 11 | } else { 12 | request.path = request.path.split('{')[0]; 13 | return request; 14 | } 15 | } 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /rs/src/main/js/api/uriTemplateInterceptor.js: -------------------------------------------------------------------------------- 1 | define(function(require) { 2 | 'use strict'; 3 | 4 | const interceptor = require('rest/interceptor'); 5 | 6 | return interceptor({ 7 | request: function (request /*, config, meta */) { 8 | /* If the URI is a URI Template per RFC 6570 (http://tools.ietf.org/html/rfc6570), trim out the template part */ 9 | if (request.path.indexOf('{') === -1) { 10 | return request; 11 | } else { 12 | request.path = request.path.split('{')[0]; 13 | return request; 14 | } 15 | } 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /as/src/main/java/io/bspk/oauth/xyz/authserver/data/api/ApprovalRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.authserver.data.api; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | 6 | import lombok.Data; 7 | import lombok.experimental.Accessors; 8 | 9 | /** 10 | * @author jricher 11 | * 12 | */ 13 | @Data 14 | @Accessors(chain = true) 15 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 16 | public class ApprovalRequest { 17 | 18 | private boolean approved; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/PushbackRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | 6 | import lombok.Data; 7 | import lombok.experimental.Accessors; 8 | 9 | /** 10 | * @author jricher 11 | * 12 | */ 13 | @Data 14 | @Accessors(chain = true) 15 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 16 | public class PushbackRequest { 17 | 18 | private String hash; 19 | private String interactRef; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/TransactionContinueRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | 6 | import lombok.Data; 7 | import lombok.experimental.Accessors; 8 | 9 | /** 10 | * @author jricher 11 | * 12 | */ 13 | @Data 14 | @Accessors(chain = true) 15 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 16 | public class TransactionContinueRequest { 17 | 18 | private String interactRef; 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /as/src/main/java/io/bspk/oauth/xyz/authserver/data/api/UserInteractionFormSubmission.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.authserver.data.api; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | 6 | import lombok.Data; 7 | import lombok.experimental.Accessors; 8 | 9 | /** 10 | * @author jricher 11 | * 12 | */ 13 | @Data 14 | @Accessors(chain = true) 15 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 16 | public class UserInteractionFormSubmission { 17 | 18 | private String userCode; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/DisplayRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | 6 | import lombok.Data; 7 | import lombok.experimental.Accessors; 8 | 9 | /** 10 | * @author jricher 11 | * 12 | */ 13 | @Data 14 | @Accessors(chain = true) 15 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 16 | public class DisplayRequest { 17 | 18 | private String name; 19 | private String uri; 20 | private String logoUri; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /as/src/main/java/io/bspk/oauth/xyz/authserver/repository/TransactionRepository.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.authserver.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | import io.bspk.oauth.xyz.data.Transaction; 6 | 7 | /** 8 | * @author jricher 9 | * 10 | */ 11 | public interface TransactionRepository extends CrudRepository { 12 | 13 | Transaction findFirstByContinueAccessTokenValue(String value); 14 | 15 | Transaction findFirstByInteractInteractId(String id); 16 | 17 | Transaction findFirstByInteractUserCode(String code); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /as/src/main/js/api/uriListConverter.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 'use strict'; 3 | 4 | /* Convert a single or array of resources into "URI1\nURI2\nURI3..." */ 5 | return { 6 | read: function(str /*, opts */) { 7 | return str.split('\n'); 8 | }, 9 | write: function(obj /*, opts */) { 10 | // If this is an Array, extract the self URI and then join using a newline 11 | if (obj instanceof Array) { 12 | return obj.map(resource => resource._links.self.href).join('\n'); 13 | } else { // otherwise, just return the self URI 14 | return obj._links.self.href; 15 | } 16 | } 17 | }; 18 | 19 | }); -------------------------------------------------------------------------------- /rc/src/main/js/api/uriListConverter.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 'use strict'; 3 | 4 | /* Convert a single or array of resources into "URI1\nURI2\nURI3..." */ 5 | return { 6 | read: function(str /*, opts */) { 7 | return str.split('\n'); 8 | }, 9 | write: function(obj /*, opts */) { 10 | // If this is an Array, extract the self URI and then join using a newline 11 | if (obj instanceof Array) { 12 | return obj.map(resource => resource._links.self.href).join('\n'); 13 | } else { // otherwise, just return the self URI 14 | return obj._links.self.href; 15 | } 16 | } 17 | }; 18 | 19 | }); -------------------------------------------------------------------------------- /rc/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9839 3 | spring: 4 | data.mongodb.uri: 5 | mongodb://host.docker.internal:27017/oauth_xyz_rc 6 | 7 | spring.data.rest.base-path: /api 8 | 9 | oauth.xyz: 10 | root: http://host.docker.internal:9839 11 | asEndpoint: http://host.docker.internal:9834/api/as/transaction 12 | rsEndpoint: http://host.docker.internal:9836/api/rs 13 | 14 | # spring.devtools.restart.exclude: 15 | 16 | spring.jackson.default-property-inclusion: NON_NULL 17 | 18 | spring.mvc.dispatch-options-request: true 19 | 20 | server.servlet.session.cookie.path: /api/client 21 | -------------------------------------------------------------------------------- /rs/src/main/js/api/uriListConverter.js: -------------------------------------------------------------------------------- 1 | define(function() { 2 | 'use strict'; 3 | 4 | /* Convert a single or array of resources into "URI1\nURI2\nURI3..." */ 5 | return { 6 | read: function(str /*, opts */) { 7 | return str.split('\n'); 8 | }, 9 | write: function(obj /*, opts */) { 10 | // If this is an Array, extract the self URI and then join using a newline 11 | if (obj instanceof Array) { 12 | return obj.map(resource => resource._links.self.href).join('\n'); 13 | } else { // otherwise, just return the self URI 14 | return obj._links.self.href; 15 | } 16 | } 17 | }; 18 | 19 | }); -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonValue; 5 | 6 | public enum ErrorCode { 7 | INVALID_REQUEST, 8 | INVALID_CLIENT, 9 | USER_DENIED, 10 | TOO_FAST, 11 | UNKNOWN_REQUEST, 12 | REQUEST_DENIED; 13 | 14 | @JsonCreator 15 | public static ErrorCode fromJson(String key) { 16 | return key == null ? null : 17 | valueOf(key.toUpperCase()); 18 | } 19 | 20 | @JsonValue 21 | public String toJson() { 22 | return name().toLowerCase(); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /as/src/main/java/io/bspk/oauth/xyz/authserver/data/api/ApprovalResponse.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.authserver.data.api; 2 | 3 | import java.net.URI; 4 | 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 6 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 7 | 8 | import lombok.Data; 9 | import lombok.experimental.Accessors; 10 | 11 | /** 12 | * @author jricher 13 | * 14 | */ 15 | @Data 16 | @Accessors(chain = true) 17 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 18 | public class ApprovalResponse { 19 | 20 | private URI uri; 21 | private boolean approved; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /as/src/main/java/io/bspk/oauth/xyz/authserver/data/api/PendingApproval.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.authserver.data.api; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | 6 | import io.bspk.oauth.xyz.data.Transaction; 7 | import lombok.Data; 8 | import lombok.experimental.Accessors; 9 | 10 | /** 11 | * @author jricher 12 | * 13 | */ 14 | @Data 15 | @Accessors(chain = true) 16 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 17 | public class PendingApproval { 18 | 19 | private Transaction transaction; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/InteractHintRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.util.Locale; 4 | import java.util.Set; 5 | 6 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 7 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 8 | 9 | import lombok.Data; 10 | import lombok.experimental.Accessors; 11 | 12 | /** 13 | * @author jricher 14 | * 15 | */ 16 | @Data 17 | @Accessors(chain = true) 18 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 19 | public class InteractHintRequest { 20 | 21 | public Set uiLocales; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/ContinueResponse.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.net.URI; 4 | 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 6 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 7 | 8 | import lombok.Data; 9 | import lombok.experimental.Accessors; 10 | 11 | /** 12 | * @author jricher 13 | * 14 | */ 15 | @Data 16 | @Accessors(chain = true) 17 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 18 | public class ContinueResponse { 19 | 20 | private Integer wait; 21 | private URI uri; 22 | private AccessTokenResponse accessToken; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /as/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9834 3 | spring: 4 | data.mongodb.uri: 5 | mongodb://host.docker.internal:27017/oauth_xyz_as 6 | 7 | spring.data.rest.base-path: /api 8 | 9 | oauth.xyz.root: http://host.docker.internal:9834 10 | 11 | # spring.devtools.restart.exclude: 12 | 13 | spring.jackson.default-property-inclusion: NON_NULL 14 | 15 | spring.mvc.dispatch-options-request: true 16 | 17 | server.servlet.session.cookie.path: /api/as/interact 18 | 19 | #logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter: DEBUG 20 | #logging.level.org.apache.cxf=DEBUG 21 | #logging.level.org.springframework: TRACE 22 | -------------------------------------------------------------------------------- /rc/src/main/js/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ReactDOM from 'react-dom'; 4 | import http from './http'; 5 | import { Button, Badge, Row, Col, Container, Card, CardImg, CardText, CardBody, CardTitle, CardSubtitle, CardHeader, Input } from 'reactstrap'; 6 | import { BrowserRouter, Switch, Route } from 'react-router-dom'; 7 | 8 | import Client from './client'; 9 | import SPA from './spa'; 10 | 11 | ReactDOM.render(( 12 | 13 | 14 | 15 | 16 | 17 | 18 | ), 19 | document.getElementById('react') 20 | ); 21 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/UserRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 6 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 7 | import com.sailpoint.ietf.subjectidentifiers.model.SubjectIdentifier; 8 | 9 | import io.bspk.oauth.xyz.data.Assertion; 10 | import lombok.Data; 11 | import lombok.experimental.Accessors; 12 | 13 | /** 14 | * @author jricher 15 | * 16 | */ 17 | @Data 18 | @Accessors(chain = true) 19 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 20 | public class UserRequest { 21 | 22 | private List subIds; 23 | private List assertions; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/http/StructuredDictionaryConverter.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.http; 2 | 3 | import org.greenbytes.http.sfv.Dictionary; 4 | import org.greenbytes.http.sfv.Parser; 5 | import org.springframework.core.convert.converter.Converter; 6 | import org.springframework.stereotype.Component; 7 | 8 | import com.google.common.base.Strings; 9 | 10 | /** 11 | * @author jricher 12 | * 13 | */ 14 | @Component 15 | public class StructuredDictionaryConverter implements Converter { 16 | 17 | @Override 18 | public Dictionary convert(String source) { 19 | if (!Strings.isNullOrEmpty(source)) { 20 | return Parser.parseDictionary(source); 21 | } else { 22 | return null; 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/crypto/KeyProofParameters.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.crypto; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import com.nimbusds.jose.jwk.JWK; 6 | 7 | import io.bspk.httpsig.HttpSigAlgorithm; 8 | import io.bspk.oauth.xyz.data.Key.Proof; 9 | import lombok.Data; 10 | import lombok.experimental.Accessors; 11 | 12 | /** 13 | * @author jricher 14 | * 15 | */ 16 | @Data 17 | @Accessors(chain = true) 18 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 19 | public class KeyProofParameters { 20 | 21 | private JWK signingKey; 22 | private Proof proof; 23 | private String digestAlgorithm; 24 | private HttpSigAlgorithm httpSigAlgorithm; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /rs/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9836 3 | spring: 4 | data.mongodb.uri: 5 | mongodb://host.docker.internal:27017/oauth_xyz_as ## This is the same data store the AS uses, so that the RS can look up token information directly 6 | 7 | spring.data.rest.base-path: /api 8 | 9 | oauth.xyz: 10 | root: http://host.docker.internal:9836/ 11 | asEndpoint: http://host.docker.internal:9834/api/as/transaction 12 | introspect: http://host.docker.internal:9834/api/as/introspect 13 | 14 | # spring.devtools.restart.exclude: 15 | 16 | spring.jackson.default-property-inclusion: NON_NULL 17 | 18 | spring.mvc.dispatch-options-request: true 19 | 20 | server.servlet.session.cookie.path: /api/rs 21 | 22 | logging.level.org.springframework.data.mongodb.core.MongoTemplate: DEBUG 23 | -------------------------------------------------------------------------------- /rc/src/main/java/io/bspk/oauth/xyz/client/repository/PendingTransactionRepository.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.client.repository; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import org.springframework.data.repository.CrudRepository; 7 | 8 | import io.bspk.oauth.xyz.data.PendingTransaction; 9 | 10 | /** 11 | * @author jricher 12 | * 13 | */ 14 | public interface PendingTransactionRepository extends CrudRepository { 15 | 16 | List findByOwner(String owner); 17 | 18 | Optional findFirstByIdAndOwner(String id, String owner); 19 | 20 | List findByCallbackIdAndOwner(String callbackId, String owner); 21 | 22 | List findByCallbackId(String callbackId); 23 | } 24 | -------------------------------------------------------------------------------- /as/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth.xyz-java", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "react": "^16.13.1", 6 | "react-dom": "^16.13.1", 7 | "rest": "^1.3.1", 8 | "reactstrap": "^7.1.0", 9 | "react-bootstrap": "^1.0.0-beta.6", 10 | "react-transition-group": "^2.2.0", 11 | "react-router-dom": "^4.3.1", 12 | "qrcode.react": "^0.9.3", 13 | "dexie": "^2.0.4", 14 | "base64url": "^3.0.1" 15 | }, 16 | "scripts": { 17 | "watch": "webpack --watch -d" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.5.0", 21 | "@babel/preset-env": "^7.5.0", 22 | "@babel/preset-react": "^7.0.0", 23 | "@babel/plugin-proposal-class-properties": "^7.0.0", 24 | "babel-loader": "^8.0.6", 25 | "webpack": "^4.39.2", 26 | "webpack-cli": "^3.1.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/json/SubjectIdentifierFormatSerializer.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.json; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.boot.jackson.JsonComponent; 6 | 7 | import com.fasterxml.jackson.core.JsonGenerator; 8 | import com.fasterxml.jackson.databind.JsonSerializer; 9 | import com.fasterxml.jackson.databind.SerializerProvider; 10 | import com.sailpoint.ietf.subjectidentifiers.model.SubjectIdentifierFormats; 11 | 12 | /** 13 | * @author jricher 14 | * 15 | */ 16 | @JsonComponent 17 | public class SubjectIdentifierFormatSerializer extends JsonSerializer { 18 | 19 | @Override 20 | public void serialize(SubjectIdentifierFormats value, JsonGenerator gen, SerializerProvider serializers) throws IOException { 21 | gen.writeObject(value.toString()); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /rs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth.xyz-java", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "react": "^16.13.1", 6 | "react-dom": "^16.13.1", 7 | "rest": "^1.3.1", 8 | "reactstrap": "^7.1.0", 9 | "react-bootstrap": "^1.0.0-beta.6", 10 | "react-transition-group": "^2.2.0", 11 | "react-router-dom": "^4.3.1", 12 | "react-icons": "3.11.0", 13 | "qrcode.react": "^0.9.3", 14 | "dexie": "^2.0.4", 15 | "base64url": "^3.0.1" 16 | }, 17 | "scripts": { 18 | "watch": "webpack --watch -d" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.5.0", 22 | "@babel/preset-env": "^7.5.0", 23 | "@babel/preset-react": "^7.0.0", 24 | "@babel/plugin-proposal-class-properties": "^7.0.0", 25 | "babel-loader": "^8.0.6", 26 | "webpack": "^4.39.2", 27 | "webpack-cli": "^3.1.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/Client.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import org.springframework.data.annotation.Id; 4 | 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 6 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 7 | 8 | import io.bspk.oauth.xyz.data.api.ClientRequest; 9 | import lombok.Data; 10 | import lombok.experimental.Accessors; 11 | 12 | /** 13 | * @author jricher 14 | * 15 | */ 16 | @Data 17 | @Accessors(chain = true) 18 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 19 | public class Client { 20 | 21 | private @Id String instanceId; 22 | private Key key; 23 | private Display display; 24 | 25 | 26 | public static Client of(ClientRequest req) { 27 | return new Client() 28 | .setKey(Key.of(req.getKey())) 29 | .setDisplay(Display.of(req.getDisplay())); 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /rc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oauth.xyz-java", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "react": "^16.13.1", 6 | "react-dom": "^16.13.1", 7 | "rest": "^1.3.1", 8 | "reactstrap": "^7.1.0", 9 | "react-bootstrap": "^1.0.0-beta.6", 10 | "react-transition-group": "^2.2.0", 11 | "react-router-dom": "^4.3.1", 12 | "react-icons": "3.11.0", 13 | "qrcode.react": "^0.9.3", 14 | "dexie": "^2.0.4", 15 | "base64url": "^3.0.1", 16 | "structured-headers": "^0.4.1" 17 | }, 18 | "scripts": { 19 | "watch": "webpack --watch -d" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.5.0", 23 | "@babel/preset-env": "^7.5.0", 24 | "@babel/preset-react": "^7.0.0", 25 | "@babel/plugin-proposal-class-properties": "^7.0.0", 26 | "babel-loader": "^8.0.6", 27 | "webpack": "^4.39.2", 28 | "webpack-cli": "^3.1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /as/src/main/js/http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const rest = require('rest'); 4 | const defaultRequest = require('rest/interceptor/defaultRequest'); 5 | const mime = require('rest/interceptor/mime'); 6 | const uriTemplateInterceptor = require('./api/uriTemplateInterceptor'); 7 | const errorCode = require('rest/interceptor/errorCode'); 8 | const baseRegistry = require('rest/mime/registry'); 9 | 10 | const registry = baseRegistry.child(); 11 | 12 | registry.register('text/uri-list', require('./api/uriListConverter')); 13 | registry.register('application/hal+json', require('rest/mime/type/application/hal')); 14 | registry.register('application/json', require('rest/mime/type/application/json')); 15 | 16 | module.exports = rest 17 | .wrap(mime, { registry: registry }) 18 | .wrap(uriTemplateInterceptor) 19 | .wrap(errorCode) 20 | .wrap(defaultRequest, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }}); -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/Display.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | 6 | import io.bspk.oauth.xyz.data.api.DisplayRequest; 7 | import lombok.Data; 8 | import lombok.experimental.Accessors; 9 | 10 | /** 11 | * @author jricher 12 | * 13 | */ 14 | @Data 15 | @Accessors(chain = true) 16 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 17 | public class Display { 18 | 19 | private String name; 20 | private String uri; 21 | private String logoUri; 22 | 23 | public static Display of(DisplayRequest displayRequest) { 24 | if (displayRequest == null) { 25 | return null; 26 | } 27 | return new Display() 28 | .setName(displayRequest.getName()) 29 | .setUri(displayRequest.getUri()) 30 | .setLogoUri(displayRequest.getLogoUri()); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /rc/src/main/js/http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const rest = require('rest'); 4 | const defaultRequest = require('rest/interceptor/defaultRequest'); 5 | const mime = require('rest/interceptor/mime'); 6 | const uriTemplateInterceptor = require('./api/uriTemplateInterceptor'); 7 | const errorCode = require('rest/interceptor/errorCode'); 8 | const baseRegistry = require('rest/mime/registry'); 9 | 10 | const registry = baseRegistry.child(); 11 | 12 | registry.register('text/uri-list', require('./api/uriListConverter')); 13 | registry.register('application/hal+json', require('rest/mime/type/application/hal')); 14 | registry.register('application/json', require('rest/mime/type/application/json')); 15 | 16 | module.exports = rest 17 | .wrap(mime, { registry: registry }) 18 | .wrap(uriTemplateInterceptor) 19 | .wrap(errorCode) 20 | .wrap(defaultRequest, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }}); -------------------------------------------------------------------------------- /rs/src/main/js/http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const rest = require('rest'); 4 | const defaultRequest = require('rest/interceptor/defaultRequest'); 5 | const mime = require('rest/interceptor/mime'); 6 | const uriTemplateInterceptor = require('./api/uriTemplateInterceptor'); 7 | const errorCode = require('rest/interceptor/errorCode'); 8 | const baseRegistry = require('rest/mime/registry'); 9 | 10 | const registry = baseRegistry.child(); 11 | 12 | registry.register('text/uri-list', require('./api/uriListConverter')); 13 | registry.register('application/hal+json', require('rest/mime/type/application/hal')); 14 | registry.register('application/json', require('rest/mime/type/application/json')); 15 | 16 | module.exports = rest 17 | .wrap(mime, { registry: registry }) 18 | .wrap(uriTemplateInterceptor) 19 | .wrap(errorCode) 20 | .wrap(defaultRequest, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }}); -------------------------------------------------------------------------------- /as/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/main/js/app.js', 5 | devtool: 'sourcemaps', 6 | cache: true, 7 | mode: 'development', 8 | output: { 9 | path: __dirname, 10 | filename: './src/main/resources/static/built/bundle.js' 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: path.join(__dirname, '.'), 16 | exclude: [/(node_modules)/, /mongo/], 17 | use: [{ 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ["@babel/preset-env", "@babel/preset-react"], 21 | "plugins": [ 22 | "@babel/plugin-proposal-class-properties" 23 | ] 24 | } 25 | }] 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /rc/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/main/js/app.js', 5 | devtool: 'sourcemaps', 6 | cache: true, 7 | mode: 'development', 8 | output: { 9 | path: __dirname, 10 | filename: './src/main/resources/static/built/bundle.js' 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: path.join(__dirname, '.'), 16 | exclude: [/(node_modules)/, /mongo/], 17 | use: [{ 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ["@babel/preset-env", "@babel/preset-react"], 21 | "plugins": [ 22 | "@babel/plugin-proposal-class-properties" 23 | ] 24 | } 25 | }] 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /rs/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/main/js/app.js', 5 | devtool: 'sourcemaps', 6 | cache: true, 7 | mode: 'development', 8 | output: { 9 | path: __dirname, 10 | filename: './src/main/resources/static/built/bundle.js' 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: path.join(__dirname, '.'), 16 | exclude: [/(node_modules)/, /mongo/], 17 | use: [{ 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ["@babel/preset-env", "@babel/preset-react"], 21 | "plugins": [ 22 | "@babel/plugin-proposal-class-properties" 23 | ] 24 | } 25 | }] 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/UserCodeResponse.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 4 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 5 | import com.google.common.base.Strings; 6 | 7 | import io.bspk.oauth.xyz.data.Interact; 8 | import lombok.Data; 9 | import lombok.experimental.Accessors; 10 | 11 | /** 12 | * @author jricher 13 | * 14 | */ 15 | @Data 16 | @Accessors(chain = true) 17 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 18 | public class UserCodeResponse { 19 | 20 | private String code; 21 | /** 22 | * @param interact 23 | * @return 24 | */ 25 | public static UserCodeResponse of(Interact interact) { 26 | if (interact == null || Strings.isNullOrEmpty(interact.getStandaloneUserCode())) { 27 | return null; 28 | } 29 | 30 | return new UserCodeResponse() 31 | .setCode(interact.getStandaloneUserCode()); 32 | 33 | } 34 | 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/Assertion.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonValue; 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 6 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 7 | 8 | import lombok.Data; 9 | import lombok.experimental.Accessors; 10 | 11 | /** 12 | * @author jricher 13 | * 14 | */ 15 | @Data 16 | @Accessors(chain = true) 17 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 18 | public class Assertion { 19 | 20 | public enum AssertionFormat { 21 | OIDC_ID_TOKEN, 22 | SAML2; 23 | 24 | @JsonCreator 25 | public static AssertionFormat fromJson(String key) { 26 | return key == null ? null : 27 | valueOf(key.toUpperCase()); 28 | } 29 | 30 | @JsonValue 31 | public String toJson() { 32 | return name().toLowerCase(); 33 | } 34 | 35 | } 36 | 37 | public AssertionFormat format; 38 | public String value; 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/User.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import java.time.Instant; 4 | 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 6 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 7 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 8 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 9 | import com.nimbusds.jwt.JWT; 10 | 11 | import io.bspk.oauth.xyz.json.JWTDeserializer; 12 | import io.bspk.oauth.xyz.json.JWTSerializer; 13 | import lombok.Data; 14 | import lombok.experimental.Accessors; 15 | 16 | /** 17 | * @author jricher 18 | * 19 | */ 20 | @Data 21 | @Accessors(chain = true) 22 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 23 | public class User { 24 | 25 | private String id; 26 | private String iss; 27 | private String email; 28 | private String phone; 29 | private Instant updatedAt; 30 | @JsonSerialize(using = JWTSerializer.class) 31 | @JsonDeserialize(using = JWTDeserializer.class) 32 | private JWT idToken; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/UserCodeUriResponse.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.net.URI; 4 | 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 6 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 7 | import com.google.common.base.Strings; 8 | 9 | import io.bspk.oauth.xyz.data.Interact; 10 | import lombok.Data; 11 | import lombok.experimental.Accessors; 12 | 13 | /** 14 | * @author jricher 15 | * 16 | */ 17 | @Data 18 | @Accessors(chain = true) 19 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 20 | public class UserCodeUriResponse { 21 | 22 | private String code; 23 | private URI uri; 24 | /** 25 | * @param interact 26 | * @return 27 | */ 28 | public static UserCodeUriResponse of(Interact interact) { 29 | if (interact == null || Strings.isNullOrEmpty(interact.getUserCode())) { 30 | return null; 31 | } 32 | 33 | return new UserCodeUriResponse() 34 | .setCode(interact.getUserCode()) 35 | .setUri(interact.getUserCodeUrl()); 36 | 37 | } 38 | 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /rs/src/main/java/io/bspk/oauth/xyz/rs/DiscoveryController.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.rs; 2 | 3 | import java.util.Map; 4 | 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.bind.annotation.CrossOrigin; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | /** 14 | * 15 | * Exists solely to expose discovery values to the front end. 16 | * 17 | * @author jricher 18 | * 19 | */ 20 | @Controller 21 | @CrossOrigin 22 | @RequestMapping("/api/whoami") 23 | public class DiscoveryController { 24 | 25 | @Value("${oauth.xyz.root}") 26 | private String baseUrl; 27 | 28 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 29 | public ResponseEntity getRoot() { 30 | 31 | Map res = Map.of("rootUrl", baseUrl); 32 | 33 | return ResponseEntity.ok(res); 34 | 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /rc/src/main/java/io/bspk/oauth/xyz/client/api/DiscoveryController.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.client.api; 2 | 3 | import java.util.Map; 4 | 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.bind.annotation.CrossOrigin; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | /** 14 | * 15 | * Exists solely to expose discovery values to the front end. 16 | * 17 | * @author jricher 18 | * 19 | */ 20 | @Controller 21 | @CrossOrigin 22 | @RequestMapping("/api/whoami") 23 | public class DiscoveryController { 24 | 25 | @Value("${oauth.xyz.root}") 26 | private String baseUrl; 27 | 28 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 29 | public ResponseEntity getRoot() { 30 | 31 | Map res = Map.of("rootUrl", baseUrl); 32 | 33 | return ResponseEntity.ok(res); 34 | 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /as/src/main/java/io/bspk/oauth/xyz/authserver/api/DiscoveryController.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.authserver.api; 2 | 3 | import java.util.Map; 4 | 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.bind.annotation.CrossOrigin; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | 13 | /** 14 | * 15 | * Exists solely to expose discovery values to the front end. 16 | * 17 | * @author jricher 18 | * 19 | */ 20 | @Controller 21 | @CrossOrigin 22 | @RequestMapping("/api/whoami") 23 | public class DiscoveryController { 24 | 25 | @Value("${oauth.xyz.root}") 26 | private String baseUrl; 27 | 28 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 29 | public ResponseEntity getRoot() { 30 | 31 | Map res = Map.of("rootUrl", baseUrl); 32 | 33 | return ResponseEntity.ok(res); 34 | 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /rs/src/main/java/io/bspk/oauth/xyz/Application.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.core.convert.converter.Converter; 9 | import org.springframework.data.mongodb.core.convert.MongoCustomConversions; 10 | 11 | import io.bspk.oauth.xyz.json.JWKDeserializer; 12 | import io.bspk.oauth.xyz.json.JWKSerializer; 13 | import io.bspk.oauth.xyz.json.JWTDeserializer; 14 | import io.bspk.oauth.xyz.json.JWTSerializer; 15 | 16 | @SpringBootApplication() 17 | public class Application { 18 | 19 | public static void main(String[] args) { 20 | SpringApplication.run(Application.class, args); 21 | } 22 | 23 | @Bean 24 | public MongoCustomConversions mongoCustomConversions() { 25 | List> list = List.of( 26 | new JWKDeserializer(), 27 | new JWKSerializer(), 28 | new JWTDeserializer(), 29 | new JWTSerializer()); 30 | return new MongoCustomConversions(list); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /rc/src/main/java/io/bspk/oauth/xyz/data/api/ClientApiRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.net.URI; 4 | import java.util.Set; 5 | 6 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 7 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 8 | import com.nimbusds.jose.jwk.JWK; 9 | 10 | import io.bspk.httpsig.HttpSigAlgorithm; 11 | import io.bspk.oauth.xyz.data.Interact.InteractStart; 12 | import io.bspk.oauth.xyz.data.Key.Proof; 13 | import lombok.Data; 14 | import lombok.experimental.Accessors; 15 | 16 | /** 17 | * @author jricher 18 | * 19 | */ 20 | @Data 21 | @Accessors(chain = true) 22 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 23 | public class ClientApiRequest { 24 | 25 | private URI grantEndpoint; 26 | private JWK privateKey; 27 | private Proof proof; 28 | private DisplayRequest display; 29 | private AccessTokenRequest accessToken; 30 | private Set interactStart; 31 | private boolean interactFinish; 32 | private UserRequest user; 33 | private SubjectRequest subject; 34 | private HttpSigAlgorithm httpSigAlgorithm; 35 | private String digest; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/InteractRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.util.Set; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 8 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 9 | 10 | import io.bspk.oauth.xyz.data.Interact.InteractStart; 11 | import io.bspk.oauth.xyz.data.InteractFinish; 12 | import lombok.Data; 13 | import lombok.experimental.Accessors; 14 | import lombok.experimental.Tolerate; 15 | 16 | /** 17 | * @author jricher 18 | * 19 | */ 20 | @Data 21 | @Accessors(chain = true) 22 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 23 | public class InteractRequest { 24 | 25 | private InteractFinish finish; 26 | @JsonProperty("start") // we need to set this for Jackson because we overload the setter, below 27 | private Set start; 28 | private InteractHintRequest hints; 29 | 30 | @Tolerate 31 | @JsonIgnore 32 | public InteractRequest setStart(InteractStart... start) { 33 | return setStart(Set.of(start)); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/json/HandleAwareFieldSerializer.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.json; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.core.JsonGenerator; 6 | import com.fasterxml.jackson.databind.JsonSerializer; 7 | import com.fasterxml.jackson.databind.SerializerProvider; 8 | 9 | import io.bspk.oauth.xyz.data.api.HandleAwareField; 10 | 11 | /** 12 | * @author jricher 13 | * 14 | */ 15 | public class HandleAwareFieldSerializer extends JsonSerializer> { 16 | 17 | /* (non-Javadoc) 18 | * @see com.fasterxml.jackson.databind.JsonSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) 19 | */ 20 | @Override 21 | public void serialize(HandleAwareField value, JsonGenerator gen, SerializerProvider serializers) throws IOException { 22 | if (value == null) { 23 | gen.writeNull(); 24 | } else if (value.isHandled()) { 25 | // it's a handle, write a string 26 | gen.writeString(value.asHandle()); 27 | } else { 28 | // it's a value, write the object 29 | gen.writeObject(value.asValue()); 30 | } 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/InteractResponse.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.net.URI; 4 | 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 6 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 7 | 8 | import io.bspk.oauth.xyz.data.Interact; 9 | import lombok.Data; 10 | import lombok.experimental.Accessors; 11 | 12 | /** 13 | * @author jricher 14 | * 15 | */ 16 | @Data 17 | @Accessors(chain = true) 18 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 19 | public class InteractResponse { 20 | 21 | private URI redirect; 22 | private URI app; 23 | private String finish; 24 | private UserCodeResponse userCode; 25 | private UserCodeUriResponse userCodeUri; 26 | 27 | 28 | public static InteractResponse of (Interact interact) { 29 | 30 | if (interact == null) { 31 | return null; 32 | } 33 | 34 | return new InteractResponse() 35 | .setRedirect(interact.getInteractionUrl()) 36 | .setApp(interact.getAppUrl()) 37 | .setFinish(interact.getServerNonce()) 38 | .setUserCode(UserCodeResponse.of(interact)) 39 | .setUserCodeUri(UserCodeUriResponse.of(interact)); 40 | 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/json/SubjectIdentifierFormatDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.json; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.boot.jackson.JsonComponent; 6 | 7 | import com.fasterxml.jackson.core.JsonParser; 8 | import com.fasterxml.jackson.core.JsonProcessingException; 9 | import com.fasterxml.jackson.databind.DeserializationContext; 10 | import com.fasterxml.jackson.databind.JsonNode; 11 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer; 12 | import com.sailpoint.ietf.subjectidentifiers.model.SubjectIdentifierFormats; 13 | 14 | /** 15 | * @author jricher 16 | * 17 | */ 18 | @JsonComponent 19 | public class SubjectIdentifierFormatDeserializer extends StdDeserializer { 20 | 21 | /** 22 | * @param vc 23 | */ 24 | protected SubjectIdentifierFormatDeserializer() { 25 | super(SubjectIdentifierFormats.class); 26 | } 27 | 28 | @Override 29 | public SubjectIdentifierFormats deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { 30 | 31 | JsonNode node = ctxt.readValue(p, JsonNode.class); 32 | 33 | return SubjectIdentifierFormats.enumByName(node.asText()); 34 | 35 | } 36 | 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /as/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | OAuth.xyz 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /rc/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | OAuth.xyz 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /rs/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | OAuth.xyz 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/ClientRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 5 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 6 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 7 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 8 | 9 | import io.bspk.oauth.xyz.json.HandleAwareFieldDeserializer; 10 | import io.bspk.oauth.xyz.json.HandleAwareFieldSerializer; 11 | import lombok.Data; 12 | import lombok.experimental.Accessors; 13 | import lombok.experimental.Tolerate; 14 | 15 | @Data 16 | @Accessors(chain = true) 17 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 18 | public class ClientRequest { 19 | 20 | @JsonSerialize(using = HandleAwareFieldSerializer.class) 21 | @JsonDeserialize(using = HandleAwareFieldDeserializer.class) 22 | private HandleAwareField key; 23 | private DisplayRequest display; 24 | 25 | @Tolerate 26 | @JsonIgnore 27 | public ClientRequest setKey(KeyRequest client) { 28 | return setKey(HandleAwareField.of(client)); 29 | } 30 | 31 | @Tolerate 32 | @JsonIgnore 33 | public ClientRequest setKey(String client) { 34 | return setKey(HandleAwareField.of(client)); 35 | } 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/json/MultipleAwareFieldSerializer.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.json; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.core.JsonGenerator; 6 | import com.fasterxml.jackson.databind.JsonSerializer; 7 | import com.fasterxml.jackson.databind.SerializerProvider; 8 | 9 | import io.bspk.oauth.xyz.data.api.MultipleAwareField; 10 | 11 | /** 12 | * @author jricher 13 | * 14 | */ 15 | public class MultipleAwareFieldSerializer extends JsonSerializer> { 16 | 17 | /* (non-Javadoc) 18 | * @see com.fasterxml.jackson.databind.JsonSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) 19 | */ 20 | @Override 21 | public void serialize(MultipleAwareField value, JsonGenerator gen, SerializerProvider serializers) throws IOException { 22 | if (value == null) { 23 | gen.writeNull(); 24 | } else if (value.isMultiple()) { 25 | // it's a multiple-value object, write out the array 26 | // since "asMultiple" returns a List, this outputs an array, not a JSON object 27 | gen.writeObject(value.asMultiple()); 28 | } else { 29 | // it's a single value, write out the value itself (usually an object) 30 | gen.writeObject(value.asSingle()); 31 | } 32 | 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /rs/src/main/js/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ReactDOM from 'react-dom'; 4 | import http from './http'; 5 | import { Button, Badge, Row, Col, Container, Card, CardImg, CardText, CardBody, CardTitle, CardSubtitle, CardHeader, Input } from 'reactstrap'; 6 | import { BrowserRouter, Switch, Route } from 'react-router-dom'; 7 | 8 | 9 | class RootPage extends React.Component { 10 | 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | rootUrl: "/api/rs" 16 | }; 17 | } 18 | 19 | componentDidMount() { 20 | document.title = "XYZ Resource Server"; 21 | this.loadRootUrl(); 22 | } 23 | 24 | loadRootUrl = () => { 25 | return http({ 26 | method: 'GET', 27 | path: '/api/whoami' 28 | }).done(response => { 29 | this.setState({ 30 | rootUrl: response.entity.rootUrl + "api/rs" 31 | }); 32 | }); 33 | } 34 | render() { 35 | return ( 36 | 37 | 38 | Resource Server 39 | 40 | 41 |

To access the resource send a GET request to {this.state.rootUrl} with an access token and associated key.

42 |
43 |
44 | ); 45 | } 46 | 47 | } 48 | 49 | 50 | 51 | ReactDOM.render(( 52 | 53 | ), 54 | document.getElementById('react') 55 | ); 56 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/json/JWKSerializer.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.json; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.core.convert.converter.Converter; 6 | import org.springframework.data.convert.WritingConverter; 7 | 8 | import com.fasterxml.jackson.core.JsonGenerator; 9 | import com.fasterxml.jackson.databind.JsonSerializer; 10 | import com.fasterxml.jackson.databind.SerializerProvider; 11 | import com.nimbusds.jose.jwk.JWK; 12 | 13 | /** 14 | * @author jricher 15 | * 16 | */ 17 | @WritingConverter 18 | public class JWKSerializer extends JsonSerializer implements Converter { 19 | 20 | /* (non-Javadoc) 21 | * @see com.fasterxml.jackson.databind.JsonSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) 22 | */ 23 | @Override 24 | public void serialize(JWK value, JsonGenerator gen, SerializerProvider serializers) throws IOException { 25 | gen.writeObject(value.toJSONObject()); 26 | } 27 | 28 | /* (non-Javadoc) 29 | * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) 30 | */ 31 | @Override 32 | public String convert(JWK source) { 33 | if (source == null) { 34 | return null; 35 | } else { 36 | return source.toJSONString(); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | XYZ is a GNAP implementation in Java. 2 | 3 | To run, first start a mongo DB instance for the items to connect to. One is provided in a docker image that opens the appropriate ports: 4 | 5 | `docker-compose up` 6 | 7 | All components connect to each other over local HTTP connections. To facilitate testing under containerization, everything has been configured to use the hostname `host.docker.internal`. To access the web interfaces from localhost, it is helpful to alias `host.docker.internal` to the loopback address of `127.0.0.1` to run this. 8 | 9 | 10 | To build locally, you'll need to install the library package from the `lib` directory by running this command from that directory: 11 | 12 | `mvn install` 13 | 14 | 15 | The authorization server is in the directory `/as/` and can be started using Spring Boot from that directory: 16 | 17 | `mvn spring-boot:run` 18 | 19 | The AS is accessible at 20 | 21 | 22 | The client instance is in the director `/c/` and can be started using Spring Boot from that directory: 23 | 24 | `mvn spring-boot:run` 25 | 26 | The client is accessible at 27 | 28 | 29 | The resources server is in the director `/rs/` and can be started using Spring Boot from that directory: 30 | 31 | `mvn spring-boot:run` 32 | 33 | The client is accessible at 34 | 35 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/InteractFinish.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import java.net.URI; 4 | 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonValue; 7 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 8 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 9 | 10 | import io.bspk.oauth.xyz.crypto.Hash.HashMethod; 11 | import lombok.Data; 12 | import lombok.experimental.Accessors; 13 | 14 | @Data 15 | @Accessors(chain = true) 16 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 17 | public class InteractFinish { 18 | public enum CallbackMethod { 19 | REDIRECT, 20 | PUSH, 21 | ; 22 | 23 | @JsonCreator 24 | public static CallbackMethod fromJson(String key) { 25 | return key == null ? null : 26 | valueOf(key.toUpperCase()); 27 | } 28 | 29 | @JsonValue 30 | public String toJson() { 31 | return name().toLowerCase(); 32 | } 33 | } 34 | 35 | private URI uri; 36 | private String nonce; 37 | private CallbackMethod method; 38 | private HashMethod hashMethod = HashMethod.SHA3; 39 | 40 | public static InteractFinish redirect() { 41 | return new InteractFinish().setMethod(CallbackMethod.REDIRECT); 42 | } 43 | 44 | public static InteractFinish pushback() { 45 | return new InteractFinish().setMethod(CallbackMethod.PUSH); 46 | } 47 | } -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/RequestedResource.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.LinkedHashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import com.fasterxml.jackson.annotation.JsonAnyGetter; 10 | import com.fasterxml.jackson.annotation.JsonAnySetter; 11 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 12 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 13 | 14 | import lombok.Data; 15 | import lombok.experimental.Accessors; 16 | 17 | /** 18 | * @author jricher 19 | * 20 | */ 21 | @Data 22 | @Accessors(chain = true) 23 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 24 | public class RequestedResource { 25 | 26 | private String type; 27 | private List actions = new ArrayList<>(); 28 | private List locations = new ArrayList<>(); 29 | private List datatypes = new ArrayList<>(); 30 | private List privileges = new ArrayList<>(); 31 | private Map other = new HashMap<>(); 32 | 33 | @JsonAnySetter 34 | public void addOther(String key, Object val) { 35 | other.put(key, val); 36 | } 37 | 38 | @JsonAnyGetter 39 | public Map getOther() { 40 | return new LinkedHashMap<>(other); //return copy 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/SubjectRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 6 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 7 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 8 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 9 | import com.sailpoint.ietf.subjectidentifiers.model.SubjectIdentifierFormats; 10 | 11 | import io.bspk.oauth.xyz.data.Assertion.AssertionFormat; 12 | import io.bspk.oauth.xyz.json.SubjectIdentifierFormatDeserializer; 13 | import io.bspk.oauth.xyz.json.SubjectIdentifierFormatSerializer; 14 | import lombok.Data; 15 | import lombok.experimental.Accessors; 16 | 17 | /** 18 | * @author jricher 19 | * 20 | */ 21 | @Data 22 | @Accessors(chain = true) 23 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 24 | public class SubjectRequest { 25 | 26 | @JsonSerialize(contentUsing = SubjectIdentifierFormatSerializer.class) 27 | @JsonDeserialize(contentUsing = SubjectIdentifierFormatDeserializer.class) 28 | private List subIdFormats; 29 | private List assertionFormats; 30 | 31 | public static SubjectRequest ofSubjectFormats(SubjectIdentifierFormats... formats) { 32 | return new SubjectRequest() 33 | .setSubIdFormats(List.of(formats)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/json/JWTSerializer.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.json; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.boot.jackson.JsonComponent; 6 | import org.springframework.core.convert.converter.Converter; 7 | import org.springframework.data.convert.WritingConverter; 8 | 9 | import com.fasterxml.jackson.core.JsonGenerator; 10 | import com.fasterxml.jackson.databind.JsonSerializer; 11 | import com.fasterxml.jackson.databind.SerializerProvider; 12 | import com.nimbusds.jwt.JWT; 13 | 14 | /** 15 | * @author jricher 16 | * 17 | */ 18 | @WritingConverter 19 | @JsonComponent 20 | public class JWTSerializer extends JsonSerializer implements Converter{ 21 | 22 | /* (non-Javadoc) 23 | * @see com.fasterxml.jackson.databind.JsonSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider) 24 | */ 25 | @Override 26 | public void serialize(JWT value, JsonGenerator gen, SerializerProvider serializers) throws IOException { 27 | if (value == null) { 28 | gen.writeNull(); 29 | } else { 30 | gen.writeString(value.serialize()); 31 | } 32 | } 33 | 34 | /* (non-Javadoc) 35 | * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) 36 | */ 37 | @Override 38 | public String convert(JWT source) { 39 | if (source == null) { 40 | return null; 41 | } else { 42 | return source.serialize(); 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/KeyRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.net.URI; 4 | import java.security.cert.X509Certificate; 5 | import java.util.Optional; 6 | 7 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 8 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 9 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 10 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 11 | import com.nimbusds.jose.jwk.JWK; 12 | 13 | import io.bspk.oauth.xyz.data.Key; 14 | import io.bspk.oauth.xyz.data.Key.Proof; 15 | import io.bspk.oauth.xyz.json.JWKDeserializer; 16 | import io.bspk.oauth.xyz.json.JWKSerializer; 17 | import lombok.Data; 18 | import lombok.experimental.Accessors; 19 | 20 | /** 21 | * @author jricher 22 | * 23 | */ 24 | @Data 25 | @Accessors(chain = true) 26 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 27 | public class KeyRequest { 28 | 29 | private Proof proof; 30 | @JsonSerialize(using = JWKSerializer.class) 31 | @JsonDeserialize(using = JWKDeserializer.class) 32 | private JWK jwk; 33 | private X509Certificate cert; 34 | private URI did; 35 | 36 | public static KeyRequest of (Key key) { 37 | return new KeyRequest() 38 | .setJwk(Optional.ofNullable(key.getJwk()).map(JWK::toPublicJWK).orElse(null)) // make sure we only ever pass a public key in the request 39 | .setCert(key.getCert()) 40 | .setDid(key.getDid()) 41 | .setProof(key.getProof()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /as/src/main/java/io/bspk/oauth/xyz/authserver/api/TransactionApi.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.authserver.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.web.bind.annotation.CrossOrigin; 11 | import org.springframework.web.bind.annotation.DeleteMapping; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PathVariable; 14 | import org.springframework.web.bind.annotation.RequestMapping; 15 | 16 | import io.bspk.oauth.xyz.authserver.repository.TransactionRepository; 17 | import io.bspk.oauth.xyz.data.Transaction; 18 | 19 | /** 20 | * @author jricher 21 | * 22 | */ 23 | @Controller 24 | @CrossOrigin 25 | @RequestMapping("/api/transaction") 26 | public class TransactionApi { 27 | @Autowired 28 | private TransactionRepository transactionRepository; 29 | 30 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE, value = "getall") 31 | public ResponseEntity> getAll() { 32 | List res = new ArrayList<>(); 33 | transactionRepository.findAll().forEach(res::add); 34 | 35 | return ResponseEntity.ok(res); 36 | } 37 | 38 | @DeleteMapping(produces = MediaType.APPLICATION_JSON_VALUE, value = "{id}") 39 | public ResponseEntity delete(@PathVariable("id") String id) { 40 | transactionRepository.deleteById(id); 41 | 42 | return ResponseEntity.noContent().build(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /as/src/main/js/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import ReactDOM from 'react-dom'; 4 | import http from './http'; 5 | import { Button, Badge, Row, Col, Container, Card, CardImg, CardText, CardBody, CardTitle, CardSubtitle, CardHeader, Input } from 'reactstrap'; 6 | import { BrowserRouter, Switch, Route } from 'react-router-dom'; 7 | 8 | import AuthServer from './authserver'; 9 | import Interact from './interact'; 10 | 11 | class RootPage extends React.Component { 12 | 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | rootUrl: "/api/as/transaction" 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | document.title = "XYZ Authorization Server"; 23 | this.loadRootUrl(); 24 | } 25 | 26 | loadRootUrl = () => { 27 | return http({ 28 | method: 'GET', 29 | path: '/api/whoami' 30 | }).done(response => { 31 | this.setState({ 32 | rootUrl: response.entity.rootUrl + "/api/as/transaction" 33 | }); 34 | }); 35 | } 36 | render() { 37 | return ( 38 | 39 | 40 | Authorization Server 41 | 42 | 43 |

GNAP Endpoint: {this.state.rootUrl}

44 |
45 |
46 | ); 47 | } 48 | 49 | } 50 | 51 | ReactDOM.render(( 52 | 53 | 54 | 55 | { 56 | () => 57 | } 58 | 59 | 60 | 61 | 62 | 63 | 64 | ), 65 | document.getElementById('react') 66 | ); 67 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/AccessToken.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import java.time.Duration; 4 | import java.time.Instant; 5 | import java.util.List; 6 | 7 | import org.apache.commons.lang3.RandomStringUtils; 8 | 9 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 10 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 11 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 12 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 13 | 14 | import io.bspk.oauth.xyz.data.api.HandleAwareField; 15 | import io.bspk.oauth.xyz.data.api.RequestedResource; 16 | import io.bspk.oauth.xyz.json.HandleAwareFieldDeserializer; 17 | import io.bspk.oauth.xyz.json.HandleAwareFieldSerializer; 18 | import lombok.Data; 19 | import lombok.experimental.Accessors; 20 | 21 | /** 22 | * @author jricher 23 | * 24 | */ 25 | @Data 26 | @Accessors(chain = true) 27 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 28 | public class AccessToken { 29 | 30 | private String value; 31 | private Key key; 32 | private boolean bound; 33 | private boolean clientBound; 34 | private String manage; 35 | @JsonSerialize(contentUsing = HandleAwareFieldSerializer.class) 36 | @JsonDeserialize(contentUsing = HandleAwareFieldDeserializer.class) 37 | private List> accessRequest; 38 | private Instant expiration; 39 | private String label; 40 | 41 | /** 42 | * Create a handle with a random value and no expiration 43 | */ 44 | public static AccessToken create() { 45 | return new AccessToken().setValue(RandomStringUtils.randomAlphanumeric(64)); 46 | } 47 | 48 | /** 49 | * Create a handle with a random value and an expiration based on the lifetime 50 | */ 51 | public static AccessToken create(Duration lifetime) { 52 | return create().setExpiration(Instant.now().plus(lifetime)); 53 | } 54 | 55 | public static AccessToken createClientBound(Key key) { 56 | return create().setKey(key) 57 | .setBound(true) 58 | .setClientBound(true); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/json/JWKDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.json; 2 | 3 | import java.io.IOException; 4 | import java.text.ParseException; 5 | 6 | import org.springframework.boot.jackson.JsonComponent; 7 | import org.springframework.core.convert.converter.Converter; 8 | import org.springframework.data.convert.ReadingConverter; 9 | 10 | import com.fasterxml.jackson.core.JsonParseException; 11 | import com.fasterxml.jackson.core.JsonParser; 12 | import com.fasterxml.jackson.core.JsonProcessingException; 13 | import com.fasterxml.jackson.databind.DeserializationContext; 14 | import com.fasterxml.jackson.databind.JsonNode; 15 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer; 16 | import com.nimbusds.jose.jwk.JWK; 17 | 18 | /** 19 | * @author jricher 20 | * 21 | */ 22 | @ReadingConverter 23 | @JsonComponent 24 | public class JWKDeserializer extends StdDeserializer implements Converter { 25 | 26 | /** 27 | * @param src 28 | */ 29 | public JWKDeserializer() { 30 | super(JWK.class); 31 | } 32 | 33 | /* (non-Javadoc) 34 | * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) 35 | */ 36 | @Override 37 | public JWK deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { 38 | 39 | JsonNode node = ctxt.readValue(p, JsonNode.class); 40 | 41 | // TODO: this might be a hack? 42 | try { 43 | return JWK.parse(node.toString()); 44 | } catch (ParseException e) { 45 | throw new JsonParseException(p, "Couldn't create JWK", e); 46 | } 47 | } 48 | 49 | /* (non-Javadoc) 50 | * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) 51 | */ 52 | @Override 53 | public JWK convert(String source) { 54 | if (source == null) { 55 | return null; 56 | } else { 57 | try { 58 | return JWK.parse(source); 59 | } catch (ParseException e) { 60 | return null; 61 | } 62 | } 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/json/JWTDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.json; 2 | 3 | import java.io.IOException; 4 | import java.text.ParseException; 5 | 6 | import org.springframework.boot.jackson.JsonComponent; 7 | import org.springframework.core.convert.converter.Converter; 8 | import org.springframework.data.convert.ReadingConverter; 9 | 10 | import com.fasterxml.jackson.core.JsonParseException; 11 | import com.fasterxml.jackson.core.JsonParser; 12 | import com.fasterxml.jackson.core.JsonProcessingException; 13 | import com.fasterxml.jackson.databind.DeserializationContext; 14 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer; 15 | import com.nimbusds.jwt.JWT; 16 | import com.nimbusds.jwt.JWTParser; 17 | 18 | /** 19 | * @author jricher 20 | * 21 | */ 22 | @ReadingConverter 23 | @JsonComponent 24 | public class JWTDeserializer extends StdDeserializer implements Converter { 25 | 26 | /** 27 | * @param src 28 | */ 29 | public JWTDeserializer() { 30 | super(JWT.class); 31 | } 32 | 33 | /* (non-Javadoc) 34 | * @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext) 35 | */ 36 | @Override 37 | public JWT deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { 38 | 39 | try { 40 | String val = ctxt.readValue(p, String.class); 41 | if (val == null) { 42 | return null; 43 | } else { 44 | return JWTParser.parse(val); 45 | } 46 | } catch (ParseException e) { 47 | throw new JsonParseException(p, "Couldn't create JWT", e); 48 | } 49 | } 50 | 51 | /* (non-Javadoc) 52 | * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) 53 | */ 54 | @Override 55 | public JWT convert(String source) { 56 | if (source == null) { 57 | return null; 58 | } else { 59 | try { 60 | return JWTParser.parse(source); 61 | } catch (ParseException e) { 62 | return null; 63 | } 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/AccessTokenRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | 9 | import com.fasterxml.jackson.annotation.JsonCreator; 10 | import com.fasterxml.jackson.annotation.JsonIgnore; 11 | import com.fasterxml.jackson.annotation.JsonValue; 12 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 13 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 14 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 15 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 16 | 17 | import io.bspk.oauth.xyz.json.HandleAwareFieldDeserializer; 18 | import io.bspk.oauth.xyz.json.HandleAwareFieldSerializer; 19 | import lombok.Data; 20 | import lombok.experimental.Accessors; 21 | 22 | /** 23 | * @author jricher 24 | * 25 | */ 26 | @Data 27 | @Accessors(chain = true) 28 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 29 | public class AccessTokenRequest { 30 | public enum TokenFlag { 31 | BEARER, 32 | SPLIT; 33 | 34 | @JsonCreator 35 | public static TokenFlag fromJson(String key) { 36 | return key == null ? null : 37 | valueOf(key.toUpperCase()); 38 | } 39 | 40 | @JsonValue 41 | public String toJson() { 42 | return name().toLowerCase(); 43 | } 44 | 45 | } 46 | 47 | @JsonSerialize(contentUsing = HandleAwareFieldSerializer.class) 48 | @JsonDeserialize(contentUsing = HandleAwareFieldDeserializer.class) 49 | private List> access = new ArrayList<>(); 50 | private String label; 51 | private Set flags; 52 | 53 | @JsonIgnore 54 | public boolean isBearer() { 55 | return flags != null && flags.contains(TokenFlag.BEARER); 56 | } 57 | 58 | public static AccessTokenRequest ofReferences(String... references) { 59 | return new AccessTokenRequest() 60 | .setAccess(Arrays.stream(references) 61 | .map(r -> HandleAwareField.of(r)) 62 | .collect(Collectors.toList())); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/AccessTokenResponse.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.time.Duration; 4 | import java.time.Instant; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import com.fasterxml.jackson.annotation.JsonInclude; 10 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 11 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 12 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 13 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 14 | 15 | import io.bspk.oauth.xyz.data.AccessToken; 16 | import io.bspk.oauth.xyz.data.Key; 17 | import io.bspk.oauth.xyz.data.api.AccessTokenRequest.TokenFlag; 18 | import io.bspk.oauth.xyz.json.HandleAwareFieldDeserializer; 19 | import io.bspk.oauth.xyz.json.HandleAwareFieldSerializer; 20 | import lombok.Data; 21 | import lombok.experimental.Accessors; 22 | 23 | /** 24 | * @author jricher 25 | * 26 | */ 27 | @Data 28 | @Accessors(chain = true) 29 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 30 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 31 | public class AccessTokenResponse { 32 | private String value; 33 | private Key key; 34 | private String manage; 35 | @JsonSerialize(contentUsing = HandleAwareFieldSerializer.class) 36 | @JsonDeserialize(contentUsing = HandleAwareFieldDeserializer.class) 37 | private List> access; 38 | private Long expiresIn; 39 | private String label; 40 | private Set flags; 41 | 42 | public static AccessTokenResponse of(AccessToken t) { 43 | if (t != null) { 44 | 45 | Set tf = new HashSet<>(); 46 | if (!t.isBound()) { 47 | tf.add(TokenFlag.BEARER); 48 | } 49 | // TODO: split and durable tokens 50 | 51 | return new AccessTokenResponse() 52 | .setValue(t.getValue()) 53 | .setFlags(tf) 54 | .setKey(!t.isClientBound() ? t.getKey() : null) 55 | .setManage(t.getManage()) 56 | .setAccess(t.getAccessRequest()) 57 | .setLabel(t.getLabel()) 58 | .setExpiresIn(t.getExpiration() != null ? 59 | Duration.between(Instant.now(), t.getExpiration()).toSeconds() 60 | : null); 61 | } else { 62 | return null; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/HandleAwareField.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 5 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 6 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 7 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 8 | 9 | import io.bspk.oauth.xyz.json.HandleAwareFieldDeserializer; 10 | import io.bspk.oauth.xyz.json.HandleAwareFieldSerializer; 11 | import lombok.AccessLevel; 12 | import lombok.Data; 13 | import lombok.Getter; 14 | import lombok.Setter; 15 | import lombok.experimental.Accessors; 16 | 17 | /** 18 | * @author jricher 19 | * 20 | */ 21 | @Data 22 | @Accessors(chain = true) 23 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 24 | @JsonSerialize(using = HandleAwareFieldSerializer.class) 25 | @JsonDeserialize(using = HandleAwareFieldDeserializer.class) 26 | public final class HandleAwareField { 27 | 28 | @Setter(AccessLevel.PRIVATE) 29 | private boolean handled; 30 | 31 | @Getter(AccessLevel.PRIVATE) 32 | @Setter(AccessLevel.PRIVATE) 33 | private String handle; 34 | 35 | @Getter(AccessLevel.PRIVATE) 36 | @Setter(AccessLevel.PRIVATE) 37 | private T data; 38 | 39 | public String asHandle() { 40 | if (isHandled()) { 41 | return handle; 42 | } else { 43 | throw new IllegalArgumentException(); 44 | } 45 | } 46 | 47 | public T asValue() { 48 | if (isHandled()) { 49 | throw new IllegalArgumentException(); 50 | } else { 51 | return data; 52 | } 53 | } 54 | 55 | public TypeReference getType() { 56 | return new TypeReference>() { }; 57 | } 58 | 59 | public static HandleAwareField of (String handle) { 60 | return new HandleAwareField() 61 | .setHandle(handle) 62 | .setHandled(true); 63 | } 64 | 65 | public static HandleAwareField of (S data) { 66 | // avoid double-wrapping that Jackson can sometimes try to do 67 | if (data instanceof HandleAwareField) { 68 | return (HandleAwareField) data; 69 | } else { 70 | return new HandleAwareField() 71 | .setData(data) 72 | .setHandled(false); 73 | } 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/Interact.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import java.net.URI; 4 | import java.util.Collections; 5 | import java.util.Optional; 6 | import java.util.Set; 7 | 8 | import com.fasterxml.jackson.annotation.JsonCreator; 9 | import com.fasterxml.jackson.annotation.JsonValue; 10 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 11 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 12 | 13 | import io.bspk.oauth.xyz.crypto.Hash.HashMethod; 14 | import io.bspk.oauth.xyz.data.InteractFinish.CallbackMethod; 15 | import io.bspk.oauth.xyz.data.api.InteractRequest; 16 | import lombok.Data; 17 | import lombok.experimental.Accessors; 18 | 19 | /** 20 | * @author jricher 21 | * 22 | */ 23 | @Data 24 | @Accessors(chain = true) 25 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 26 | public class Interact { 27 | 28 | public enum InteractStart { 29 | REDIRECT, 30 | APP, 31 | USER_CODE, 32 | USER_CODE_URI; 33 | 34 | @JsonCreator 35 | public static InteractStart fromJson(String key) { 36 | return key == null ? null : 37 | valueOf(key.toUpperCase()); 38 | } 39 | 40 | @JsonValue 41 | public String toJson() { 42 | return name().toLowerCase(); 43 | } 44 | } 45 | 46 | private Set startMethods = Collections.emptySet(); 47 | private URI interactionUrl; 48 | private URI appUrl; 49 | private String interactId; 50 | private String serverNonce; 51 | private String clientNonce; 52 | private URI callbackUri; 53 | private String interactRef; 54 | private String standaloneUserCode; 55 | private String userCode; 56 | private URI userCodeUrl; 57 | private CallbackMethod callbackMethod; 58 | private HashMethod callbackHashMethod; 59 | 60 | /** 61 | * @param interact 62 | * @return 63 | */ 64 | public static Interact of(InteractRequest interact) { 65 | 66 | Optional interactFinish = Optional.ofNullable(interact.getFinish()); 67 | 68 | return new Interact() 69 | .setStartMethods(Optional.ofNullable(interact.getStart()).orElse(Collections.emptySet())) 70 | .setCallbackMethod(interactFinish.map(InteractFinish::getMethod).orElse(null)) 71 | .setCallbackUri(interactFinish.map(InteractFinish::getUri).orElse(null)) 72 | .setClientNonce(interactFinish.map(InteractFinish::getNonce).orElse(null)) 73 | .setCallbackHashMethod(interactFinish.map(InteractFinish::getHashMethod).orElse(null)); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/Key.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import java.net.URI; 4 | import java.security.cert.X509Certificate; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.data.annotation.Transient; 9 | 10 | import com.fasterxml.jackson.annotation.JsonCreator; 11 | import com.fasterxml.jackson.annotation.JsonIgnore; 12 | import com.fasterxml.jackson.annotation.JsonValue; 13 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 14 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 15 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 16 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 17 | import com.nimbusds.jose.jwk.JWK; 18 | 19 | import io.bspk.oauth.xyz.data.api.HandleAwareField; 20 | import io.bspk.oauth.xyz.data.api.KeyRequest; 21 | import io.bspk.oauth.xyz.json.JWKDeserializer; 22 | import io.bspk.oauth.xyz.json.JWKSerializer; 23 | import lombok.Data; 24 | import lombok.experimental.Accessors; 25 | 26 | /** 27 | * @author jricher 28 | * 29 | */ 30 | @Data 31 | @Accessors(chain = true) 32 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 33 | public class Key { 34 | public enum Proof { 35 | JWSD, 36 | MTLS, 37 | HTTPSIG, 38 | DPOP, 39 | OAUTHPOP, 40 | JWS 41 | ; 42 | 43 | @JsonCreator 44 | public static Proof fromJson(String key) { 45 | return key == null ? null : 46 | valueOf(key.toUpperCase()); 47 | } 48 | 49 | @JsonValue 50 | public String toJson() { 51 | return name().toLowerCase(); 52 | } 53 | 54 | } 55 | 56 | @JsonIgnore 57 | @Transient 58 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 59 | 60 | 61 | private Proof proof; 62 | @JsonSerialize(using = JWKSerializer.class) 63 | @JsonDeserialize(using = JWKDeserializer.class) 64 | private JWK jwk; 65 | private X509Certificate cert; 66 | private URI did; 67 | 68 | 69 | public static Key of(HandleAwareField request) { 70 | 71 | if (request == null) { 72 | return null; 73 | } 74 | 75 | if (request.isHandled()) { 76 | // TODO: dereference keys using a service 77 | return null; 78 | } 79 | 80 | KeyRequest kr = request.asValue(); 81 | 82 | if (kr == null) { 83 | return null; 84 | } 85 | 86 | return new Key() 87 | .setProof(kr.getProof()) 88 | .setCert(kr.getCert()) 89 | .setDid(kr.getDid()) 90 | .setJwk(kr.getJwk()); 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/TransactionRequest.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 5 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 6 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 7 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 8 | 9 | import io.bspk.oauth.xyz.json.HandleAwareFieldDeserializer; 10 | import io.bspk.oauth.xyz.json.HandleAwareFieldSerializer; 11 | import io.bspk.oauth.xyz.json.MultipleAwareFieldDeserializer; 12 | import io.bspk.oauth.xyz.json.MultipleAwareFieldSerializer; 13 | import lombok.Data; 14 | import lombok.experimental.Accessors; 15 | import lombok.experimental.Tolerate; 16 | 17 | /** 18 | * @author jricher 19 | * 20 | */ 21 | @Data 22 | @Accessors(chain = true) 23 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 24 | public class TransactionRequest { 25 | 26 | private InteractRequest interact; 27 | @JsonSerialize(using = HandleAwareFieldSerializer.class) 28 | @JsonDeserialize(using = HandleAwareFieldDeserializer.class) 29 | private HandleAwareField client; 30 | @JsonSerialize(using = HandleAwareFieldSerializer.class) 31 | @JsonDeserialize(using = HandleAwareFieldDeserializer.class) 32 | private HandleAwareField user; 33 | @JsonSerialize(using = MultipleAwareFieldSerializer.class) 34 | @JsonDeserialize(using = MultipleAwareFieldDeserializer.class) 35 | private MultipleAwareField accessToken; 36 | private SubjectRequest subject; 37 | 38 | @Tolerate 39 | @JsonIgnore 40 | public TransactionRequest setClient(ClientRequest client) { 41 | return setClient(HandleAwareField.of(client)); 42 | } 43 | 44 | @Tolerate 45 | @JsonIgnore 46 | public TransactionRequest setClient(String client) { 47 | return setClient(HandleAwareField.of(client)); 48 | } 49 | 50 | @Tolerate 51 | @JsonIgnore 52 | public TransactionRequest setUser(UserRequest user) { 53 | return setUser(HandleAwareField.of(user)); 54 | } 55 | 56 | @Tolerate 57 | @JsonIgnore 58 | public TransactionRequest setUser(String user) { 59 | return setUser(HandleAwareField.of(user)); 60 | } 61 | 62 | @Tolerate 63 | @JsonIgnore 64 | public TransactionRequest setAccessToken(AccessTokenRequest accessToken) { 65 | return setAccessToken(MultipleAwareField.of(accessToken)); 66 | } 67 | 68 | @Tolerate 69 | @JsonIgnore 70 | public TransactionRequest setAccessToken(AccessTokenRequest... accessToken) { 71 | return setAccessToken(MultipleAwareField.of(accessToken)); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/MultipleAwareField.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.function.Function; 6 | import java.util.stream.Collectors; 7 | 8 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 9 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 10 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 11 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 12 | 13 | import io.bspk.oauth.xyz.json.MultipleAwareFieldDeserializer; 14 | import io.bspk.oauth.xyz.json.MultipleAwareFieldSerializer; 15 | import lombok.AccessLevel; 16 | import lombok.Data; 17 | import lombok.Getter; 18 | import lombok.Setter; 19 | import lombok.experimental.Accessors; 20 | 21 | /** 22 | * @author jricher 23 | * 24 | */ 25 | @Data 26 | @Accessors(chain = true) 27 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 28 | @JsonSerialize(using = MultipleAwareFieldSerializer.class) 29 | @JsonDeserialize(using = MultipleAwareFieldDeserializer.class) 30 | public class MultipleAwareField { 31 | 32 | @Setter(AccessLevel.PRIVATE) 33 | private boolean multiple; 34 | 35 | @Getter(AccessLevel.PRIVATE) 36 | @Setter(AccessLevel.PRIVATE) 37 | private List data = new ArrayList<>(); 38 | 39 | public T asSingle() { 40 | if (isMultiple()) { 41 | throw new IllegalArgumentException(); 42 | } else { 43 | return data.get(0); 44 | } 45 | } 46 | 47 | public List asMultiple() { 48 | if (isMultiple()) { 49 | // don't give direct access to manipulate the list 50 | return new ArrayList<>(data); 51 | } else { 52 | throw new IllegalArgumentException(); 53 | } 54 | } 55 | 56 | public static MultipleAwareField of(S singleton) { 57 | MultipleAwareField f = new MultipleAwareField() 58 | .setMultiple(false); 59 | 60 | f.getData().add(singleton); 61 | 62 | return f; 63 | } 64 | 65 | public static MultipleAwareField of (MultipleAwareField input, Function fn) { 66 | if (input == null || fn == null) { 67 | return null; 68 | } else { 69 | return new MultipleAwareField() 70 | .setData(input.getData().stream() 71 | .map(fn) 72 | .collect(Collectors.toList())) 73 | .setMultiple(input.isMultiple()); 74 | } 75 | } 76 | 77 | public static MultipleAwareField of(List items) { 78 | MultipleAwareField f = new MultipleAwareField() 79 | .setMultiple(true); 80 | 81 | f.getData().addAll(items); 82 | 83 | return f; 84 | } 85 | 86 | public static MultipleAwareField of(S... items) { 87 | return of(List.of(items)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/json/HandleAwareFieldDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.json; 2 | 3 | import java.io.IOException; 4 | 5 | import com.fasterxml.jackson.core.JsonParseException; 6 | import com.fasterxml.jackson.core.JsonParser; 7 | import com.fasterxml.jackson.core.JsonProcessingException; 8 | import com.fasterxml.jackson.core.JsonToken; 9 | import com.fasterxml.jackson.core.type.TypeReference; 10 | import com.fasterxml.jackson.databind.BeanProperty; 11 | import com.fasterxml.jackson.databind.DeserializationContext; 12 | import com.fasterxml.jackson.databind.JavaType; 13 | import com.fasterxml.jackson.databind.JsonDeserializer; 14 | import com.fasterxml.jackson.databind.JsonMappingException; 15 | import com.fasterxml.jackson.databind.deser.ContextualDeserializer; 16 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer; 17 | 18 | import io.bspk.oauth.xyz.data.api.HandleAwareField; 19 | import lombok.Getter; 20 | import lombok.Setter; 21 | 22 | /** 23 | * @author jricher 24 | * 25 | */ 26 | public class HandleAwareFieldDeserializer extends StdDeserializer> implements ContextualDeserializer { 27 | 28 | @Getter 29 | @Setter 30 | private JavaType valueType; 31 | 32 | public HandleAwareFieldDeserializer() { 33 | super (HandleAwareField.class); 34 | } 35 | 36 | @Override 37 | public HandleAwareField deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { 38 | JsonToken token = p.currentToken(); 39 | 40 | if (token == JsonToken.VALUE_STRING) { 41 | // it's a string, parse it as a handle reference 42 | String value = p.readValueAs(new TypeReference() {}); 43 | 44 | return HandleAwareField.of(value); 45 | 46 | } else if (token == JsonToken.START_OBJECT) { 47 | // it's an object, parse it as a value reference 48 | T value = ctxt.readValue(p, getValueType()); 49 | 50 | return HandleAwareField.of(value); 51 | } else { 52 | throw new JsonParseException(p, "Couldn't convert from JSON node type"); 53 | } 54 | } 55 | 56 | @Override 57 | public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { 58 | JavaType wrapperType = null; 59 | if (property != null) { 60 | wrapperType = property.getType(); 61 | } else { 62 | wrapperType = ctxt.getContextualType(); 63 | } 64 | JavaType valueType = wrapperType.containedType(0); // this is the parameterized value's type 65 | HandleAwareFieldDeserializer deserializer = new HandleAwareFieldDeserializer<>(); 66 | deserializer.setValueType(valueType); 67 | return deserializer; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/Subject.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import java.time.Instant; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 8 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 9 | import com.sailpoint.ietf.subjectidentifiers.model.SubjectIdentifier; 10 | import com.sailpoint.ietf.subjectidentifiers.model.SubjectIdentifierFormats; 11 | 12 | import io.bspk.oauth.xyz.data.Assertion.AssertionFormat; 13 | import io.bspk.oauth.xyz.data.api.SubjectRequest; 14 | import lombok.Data; 15 | import lombok.experimental.Accessors; 16 | 17 | /** 18 | * @author jricher 19 | * 20 | */ 21 | @Data 22 | @Accessors(chain = true) 23 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 24 | public class Subject { 25 | 26 | private List subIds; 27 | private List assertions; 28 | private Instant updatedAt; 29 | 30 | public static Subject of(SubjectRequest request, User user) { 31 | Subject c = new Subject(); 32 | List subIdsRequest = request.getSubIdFormats(); 33 | List subIds = new ArrayList<>(); 34 | 35 | if (subIdsRequest != null) { 36 | if (subIdsRequest.contains(SubjectIdentifierFormats.ISSUER_SUBJECT)) { 37 | subIds.add(new SubjectIdentifier.Builder() 38 | .format(SubjectIdentifierFormats.ISSUER_SUBJECT) 39 | .subject(user.getId()) 40 | .issuer(user.getIss()) 41 | .build()); 42 | } 43 | if (subIdsRequest.contains(SubjectIdentifierFormats.EMAIL)) { 44 | subIds.add(new SubjectIdentifier.Builder() 45 | .format(SubjectIdentifierFormats.EMAIL) 46 | .email(user.getEmail()) 47 | .build()); 48 | } 49 | if (subIdsRequest.contains(SubjectIdentifierFormats.PHONE_NUMBER)) { 50 | subIds.add(new SubjectIdentifier.Builder() 51 | .format(SubjectIdentifierFormats.PHONE_NUMBER) 52 | .phoneNumber(user.getPhone()) 53 | .build()); 54 | } 55 | if (subIdsRequest.contains(SubjectIdentifierFormats.OPAQUE)) { 56 | subIds.add(new SubjectIdentifier.Builder() 57 | .format(SubjectIdentifierFormats.OPAQUE) 58 | .id(user.getId()) 59 | .build()); 60 | } 61 | // TODO: add other types 62 | 63 | c.setSubIds(subIds); 64 | } 65 | 66 | List assertionRequest = request.getAssertionFormats(); 67 | List assertions = new ArrayList<>(); 68 | 69 | if (assertionRequest != null) { 70 | if (assertionRequest.contains(AssertionFormat.OIDC_ID_TOKEN) && user.getIdToken() != null) { 71 | assertions.add(new Assertion() 72 | .setFormat(AssertionFormat.OIDC_ID_TOKEN) 73 | .setValue(user.getIdToken().serialize())); 74 | } 75 | // TODO, add additional formats 76 | c.setAssertions(assertions); 77 | } 78 | 79 | c.setUpdatedAt(user.getUpdatedAt()); 80 | return c; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/Transaction.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.annotation.Id; 6 | 7 | import com.fasterxml.jackson.annotation.JsonCreator; 8 | import com.fasterxml.jackson.annotation.JsonIgnore; 9 | import com.fasterxml.jackson.annotation.JsonValue; 10 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 11 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 12 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 13 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 14 | 15 | import io.bspk.oauth.xyz.data.api.AccessTokenRequest; 16 | import io.bspk.oauth.xyz.data.api.MultipleAwareField; 17 | import io.bspk.oauth.xyz.data.api.SubjectRequest; 18 | import io.bspk.oauth.xyz.json.MultipleAwareFieldDeserializer; 19 | import io.bspk.oauth.xyz.json.MultipleAwareFieldSerializer; 20 | import lombok.Data; 21 | import lombok.NonNull; 22 | import lombok.experimental.Accessors; 23 | import lombok.experimental.Tolerate; 24 | 25 | /** 26 | * @author jricher 27 | * 28 | */ 29 | @Data 30 | @Accessors(chain = true) 31 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 32 | public class Transaction { 33 | 34 | public enum Status { 35 | 36 | NEW, // newly created transaction, nothing's been done to it yet 37 | ISSUED, // an access token has been issued 38 | AUTHORIZED, // the user has authorized but a token has not been issued yet 39 | WAITING, // we are waiting for the user 40 | DENIED; // the user denied the transaction 41 | 42 | @JsonCreator 43 | public static Status fromJson(String key) { 44 | return key == null ? null : 45 | valueOf(key.toUpperCase()); 46 | } 47 | 48 | @JsonValue 49 | public String toJson() { 50 | return name().toLowerCase(); 51 | } 52 | 53 | } 54 | 55 | private @Id String id; 56 | private Display display; 57 | private User user; 58 | private Interact interact; 59 | private String interactHandle; 60 | private AccessToken continueAccessToken; 61 | @JsonSerialize(using = MultipleAwareFieldSerializer.class) 62 | @JsonDeserialize(using = MultipleAwareFieldDeserializer.class) 63 | private MultipleAwareField accessToken; 64 | private @NonNull Status status = Status.NEW; 65 | private Key key; 66 | private Subject subject; 67 | private SubjectRequest subjectRequest; 68 | @JsonSerialize(using = MultipleAwareFieldSerializer.class) 69 | @JsonDeserialize(using = MultipleAwareFieldDeserializer.class) 70 | private MultipleAwareField accessTokenRequest; 71 | 72 | @JsonIgnore 73 | @Tolerate 74 | public Transaction setAccessToken(AccessToken t) { 75 | return setAccessToken(MultipleAwareField.of(t)); 76 | } 77 | 78 | @JsonIgnore 79 | @Tolerate 80 | public Transaction setAccessToken(List t) { 81 | return setAccessToken(MultipleAwareField.of(t)); 82 | } 83 | 84 | 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/data/api/TransactionResponse.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data.api; 2 | 3 | import java.net.URI; 4 | 5 | import org.springframework.beans.factory.annotation.Value; 6 | 7 | import com.fasterxml.jackson.annotation.JsonIgnore; 8 | import com.fasterxml.jackson.annotation.JsonProperty; 9 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 10 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 11 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 12 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 13 | 14 | import io.bspk.oauth.xyz.data.Subject; 15 | import io.bspk.oauth.xyz.data.Transaction; 16 | import io.bspk.oauth.xyz.json.MultipleAwareFieldDeserializer; 17 | import io.bspk.oauth.xyz.json.MultipleAwareFieldSerializer; 18 | import lombok.Data; 19 | import lombok.experimental.Accessors; 20 | import lombok.experimental.Tolerate; 21 | 22 | /** 23 | * @author jricher 24 | * 25 | */ 26 | @Data 27 | @Accessors(chain = true) 28 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 29 | public class TransactionResponse { 30 | 31 | @JsonProperty("continue") // "continue" is a java keyword 32 | private ContinueResponse cont; 33 | private String instanceId; 34 | @JsonSerialize(using = MultipleAwareFieldSerializer.class) 35 | @JsonDeserialize(using = MultipleAwareFieldDeserializer.class) 36 | private MultipleAwareField accessToken; 37 | private Subject subject; 38 | private InteractResponse interact; 39 | private ErrorCode error; 40 | private String errorDescription; 41 | 42 | @Value("${oauth.xyz.root}") 43 | private String baseUrl; 44 | 45 | @Tolerate 46 | @JsonIgnore 47 | public TransactionResponse setAccessToken(AccessTokenResponse accessToken) { 48 | return setAccessToken(MultipleAwareField.of(accessToken)); 49 | } 50 | 51 | @Tolerate 52 | @JsonIgnore 53 | public TransactionResponse setAccessToken(AccessTokenResponse... accessToken) { 54 | return setAccessToken(MultipleAwareField.of(accessToken)); 55 | } 56 | 57 | public static TransactionResponse of(Transaction t, URI continueUri) { 58 | return of(t, null, continueUri); 59 | } 60 | 61 | 62 | public static TransactionResponse of(Transaction t, String instanceId, URI continueUri) { 63 | 64 | return new TransactionResponse() 65 | .setAccessToken(MultipleAwareField.of(t.getAccessToken(), AccessTokenResponse::of)) 66 | .setInteract(InteractResponse.of(t.getInteract())) 67 | .setCont(new ContinueResponse() 68 | .setAccessToken(AccessTokenResponse.of(t.getContinueAccessToken())) 69 | .setUri(continueUri)) 70 | .setInstanceId(instanceId) 71 | .setSubject(t.getSubject()); 72 | 73 | } 74 | 75 | public static TransactionResponse of(ErrorCode e) { 76 | return of(e, null); 77 | } 78 | 79 | public static TransactionResponse of(ErrorCode e, String errorDescription) { 80 | return new TransactionResponse() 81 | .setError(e) 82 | .setErrorDescription(errorDescription); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/json/MultipleAwareFieldDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.json; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | import com.fasterxml.jackson.core.JsonParseException; 7 | import com.fasterxml.jackson.core.JsonParser; 8 | import com.fasterxml.jackson.core.JsonProcessingException; 9 | import com.fasterxml.jackson.core.JsonToken; 10 | import com.fasterxml.jackson.databind.BeanProperty; 11 | import com.fasterxml.jackson.databind.DeserializationContext; 12 | import com.fasterxml.jackson.databind.JavaType; 13 | import com.fasterxml.jackson.databind.JsonDeserializer; 14 | import com.fasterxml.jackson.databind.JsonMappingException; 15 | import com.fasterxml.jackson.databind.deser.ContextualDeserializer; 16 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer; 17 | 18 | import io.bspk.oauth.xyz.data.api.MultipleAwareField; 19 | import lombok.Getter; 20 | import lombok.Setter; 21 | 22 | /** 23 | * @author jricher 24 | * 25 | */ 26 | public class MultipleAwareFieldDeserializer extends StdDeserializer> implements ContextualDeserializer { 27 | 28 | @Getter 29 | @Setter 30 | private JavaType valueType; 31 | 32 | @Getter 33 | @Setter 34 | private JavaType listValueType; 35 | 36 | public MultipleAwareFieldDeserializer() { 37 | super (MultipleAwareField.class); 38 | } 39 | 40 | @Override 41 | public MultipleAwareField deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { 42 | JsonToken token = p.currentToken(); 43 | 44 | if (token == JsonToken.START_ARRAY) { 45 | // it's an array, parse it as a multi-valued element 46 | List value = ctxt.readValue(p, getListValueType()); 47 | 48 | return MultipleAwareField.of(value); 49 | 50 | } else if (token == JsonToken.START_OBJECT) { 51 | // it's an object, parse it as a single value 52 | T value = ctxt.readValue(p, getValueType()); 53 | 54 | return MultipleAwareField.of(value); 55 | } else { 56 | throw new JsonParseException(p, "Couldn't convert from JSON node type"); 57 | } 58 | } 59 | 60 | @Override 61 | public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { 62 | JavaType wrapperType = null; 63 | if (property != null) { 64 | wrapperType = property.getType(); 65 | } else { 66 | wrapperType = ctxt.getContextualType(); 67 | } 68 | JavaType valueType = wrapperType.containedType(0); // this is the parameterized value's type 69 | 70 | JavaType listValueType = ctxt.getTypeFactory().constructCollectionType(List.class, valueType); // this is a type for a list of the parameterized value's type 71 | 72 | MultipleAwareFieldDeserializer deserializer = new MultipleAwareFieldDeserializer<>(); 73 | deserializer.setValueType(valueType); 74 | deserializer.setListValueType(listValueType); 75 | 76 | return deserializer; 77 | } 78 | 79 | 80 | } 81 | -------------------------------------------------------------------------------- /rc/src/main/java/io/bspk/oauth/xyz/Application.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz; 2 | 3 | import java.text.ParseException; 4 | import java.util.List; 5 | 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.core.convert.converter.Converter; 10 | import org.springframework.data.mongodb.core.convert.MongoCustomConversions; 11 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 12 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 13 | 14 | import com.nimbusds.jose.jwk.JWK; 15 | 16 | import io.bspk.oauth.xyz.json.JWKDeserializer; 17 | import io.bspk.oauth.xyz.json.JWKSerializer; 18 | import io.bspk.oauth.xyz.json.JWTDeserializer; 19 | import io.bspk.oauth.xyz.json.JWTSerializer; 20 | 21 | @SpringBootApplication() 22 | public class Application { 23 | 24 | public static void main(String[] args) { 25 | SpringApplication.run(Application.class, args); 26 | } 27 | 28 | @Bean 29 | public JWK clientKey() { 30 | try { 31 | return JWK.parse( 32 | "{\n" + 33 | " \"kty\": \"RSA\",\n" + 34 | " \"d\": \"m1M7uj1uZMgQqd2qwqBk07rgFzbzdCAbsfu5kvqoALv3oRdyi_UVHXDhos3DZVQ3M6mKgb30XXESykY8tpWcQOU-qx6MwtSFbo-3SNx9fBtylyQosHECGyleVP79YTE4mC0odRoUIDS90J9AcFsdVtC6M2oJ3CCL577a-lJg6eYyQoRmbjdzqMnBFJ99TCfR6wBQQbzXi1K_sN6gcqhxMmQXHWlqfT7-AJIxX9QUF0rrXMMX9fPh-HboGKs2Dqoo3ofJ2XuePpmpVDvtGy_jenXmUdpsRleqnMrEI2qkBonJQSKL4HPNpsylbQyXt2UtYrzcopCp7jL-j56kRPpQAQ\",\n" + 35 | " \"e\": \"AQAB\",\n" + 36 | " \"kid\": \"xyz-client\",\n" + 37 | " \"alg\": \"RS256\",\n" + 38 | " \"n\": \"zwCT_3bx-glbbHrheYpYpRWiY9I-nEaMRpZnRrIjCs6b_emyTkBkDDEjSysi38OC73hj1-WgxcPdKNGZyIoH3QZen1MKyyhQpLJG1-oLNLqm7pXXtdYzSdC9O3-oiyy8ykO4YUyNZrRRfPcihdQCbO_OC8Qugmg9rgNDOSqppdaNeas1ov9PxYvxqrz1-8Ha7gkD00YECXHaB05uMaUadHq-O_WIvYXicg6I5j6S44VNU65VBwu-AlynTxQdMAWP3bYxVVy6p3-7eTJokvjYTFqgDVDZ8lUXbr5yCTnRhnhJgvf3VjD_malNe8-tOqK5OSDlHTy6gD9NqdGCm-Pm3Q\"\n" + 39 | "}" 40 | ); 41 | } catch (ParseException e) { 42 | return null; 43 | } 44 | } 45 | 46 | @Bean 47 | public JWK clientKey2() { 48 | try { 49 | return JWK.parse( 50 | "{\n" 51 | + " \"kty\": \"EC\",\n" 52 | + " \"d\": \"e0ClWmBfEJVMUSCp6ewnMprJtfvDiZ8HGi3u40upAvQ\",\n" 53 | + " \"use\": \"sig\",\n" 54 | + " \"crv\": \"secp256k1\",\n" 55 | + " \"kid\": \"sig-2020-10-30T20:47:51Z\",\n" 56 | + " \"x\": \"JtH74-28f8tBlYd3SCm7RUVAkOWMj702li03oAo_GnY\",\n" 57 | + " \"y\": \"Y-R9g1bZz754iZn9etHDuCOoKz_1C4HCh2LF0lK85qk\",\n" 58 | + " \"alg\": \"ES256K\"\n" 59 | + "}" 60 | ); 61 | } catch (ParseException e) { 62 | return null; 63 | } 64 | } 65 | 66 | @Bean 67 | public MongoCustomConversions mongoCustomConversions() { 68 | List> list = List.of( 69 | new JWKDeserializer(), 70 | new JWKSerializer(), 71 | new JWTDeserializer(), 72 | new JWTSerializer()); 73 | return new MongoCustomConversions(list); 74 | } 75 | 76 | @Bean 77 | public WebMvcConfigurer webMvcConfigurer() { 78 | WebMvcConfigurer configurer = new WebMvcConfigurer() { 79 | @Override 80 | public void addViewControllers(ViewControllerRegistry registry) { 81 | 82 | // map all of the front-end pages to React 83 | 84 | registry.addViewController("/spa").setViewName("/"); 85 | registry.addViewController("/spa/*").setViewName("/"); 86 | } 87 | }; 88 | 89 | return configurer; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/crypto/Hash.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.crypto; 2 | 3 | import java.net.URI; 4 | import java.security.MessageDigest; 5 | import java.util.Base64; 6 | import java.util.function.Function; 7 | 8 | import org.bouncycastle.jcajce.provider.digest.SHA1; 9 | import org.bouncycastle.jcajce.provider.digest.SHA256; 10 | import org.bouncycastle.jcajce.provider.digest.SHA3; 11 | import org.bouncycastle.jcajce.provider.digest.SHA512; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import com.fasterxml.jackson.annotation.JsonCreator; 16 | import com.fasterxml.jackson.annotation.JsonValue; 17 | import com.google.common.base.Joiner; 18 | import com.nimbusds.jose.util.Base64URL; 19 | 20 | import lombok.AllArgsConstructor; 21 | import lombok.Getter; 22 | 23 | /** 24 | * @author jricher 25 | * 26 | */ 27 | public abstract class Hash { 28 | 29 | private static final Logger log = LoggerFactory.getLogger(Hash.class); 30 | 31 | @AllArgsConstructor 32 | public enum HashMethod { 33 | SHA3("sha3", Hash::SHA3_512_encode), 34 | SHA2("sha2", Hash::SHA2_512_encode) 35 | ; 36 | 37 | @Getter private String name; 38 | @Getter private Function function; 39 | 40 | @JsonCreator 41 | public static HashMethod fromJson(String key) { 42 | return key == null ? null : 43 | valueOf(key.toUpperCase()); 44 | } 45 | 46 | @JsonValue 47 | public String toJson() { 48 | return name().toLowerCase(); 49 | } 50 | 51 | } 52 | 53 | public static String SHA3_512_encode(String input) { 54 | MessageDigest digest = new SHA3.Digest512(); 55 | byte[] output = digest.digest(input.getBytes()); 56 | 57 | byte[] encoded = Base64.getUrlEncoder().withoutPadding().encode(output); 58 | 59 | return new String(encoded); 60 | 61 | } 62 | 63 | public static String SHA2_512_encode(String input) { 64 | byte[] output = SHA2_512_digest(input.getBytes()); 65 | 66 | byte[] encoded = Base64.getUrlEncoder().withoutPadding().encode(output); 67 | 68 | return new String(encoded); 69 | 70 | } 71 | 72 | public static byte[] SHA2_512_digest(byte[] input) { 73 | MessageDigest digest = new SHA512.Digest(); 74 | byte[] output = digest.digest(input); 75 | return output; 76 | } 77 | 78 | public static String calculateInteractHash(String clientNonce, String serverNonce, String interact, URI endpointUri, HashMethod hashMethod) { 79 | return hashMethod.getFunction().apply( 80 | Joiner.on('\n') 81 | .join(clientNonce, 82 | serverNonce, 83 | interact, 84 | endpointUri.toString())); 85 | } 86 | 87 | public static String SHA256_encode(String input) { 88 | if (input == null || input.isEmpty()) { 89 | return null; 90 | } 91 | 92 | MessageDigest digest = new SHA256.Digest(); 93 | byte[] output = digest.digest(input.getBytes()); 94 | 95 | byte[] encoded = Base64.getUrlEncoder().withoutPadding().encode(output); 96 | 97 | return new String(encoded); 98 | } 99 | 100 | public static String SHA1_digest(byte[] input) { 101 | if (input == null || input.length == 0) { 102 | return null; 103 | } 104 | 105 | MessageDigest digest = new SHA1.Digest(); 106 | byte[] output = digest.digest(input); 107 | 108 | byte[] encoded = Base64.getEncoder().encode(output); 109 | 110 | return new String(encoded); 111 | } 112 | 113 | // does a sha256 hash 114 | public static Base64URL SHA256_encode_url(byte[] input) { 115 | if (input == null || input.length == 0) { 116 | return null; 117 | } 118 | 119 | MessageDigest digest = new SHA256.Digest(); 120 | byte[] output = digest.digest(input); 121 | 122 | Base64URL encodedHash = Base64URL.encode(output); 123 | 124 | return encodedHash; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /lib/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | io.bspk 7 | oauth.xyz.lib 8 | 0.1.4-SNAPSHOT 9 | oauth.xyz-java 10 | Transactional Authorization Strawman 11 | 12 | 13 | 14 14 | 14 15 | 14 16 | 17 | 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-dependencies 23 | 2.3.12.RELEASE 24 | pom 25 | import 26 | 27 | 28 | com.nimbusds 29 | nimbus-jose-jwt 30 | 9.4.1 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-mongodb 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-web 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-devtools 48 | runtime 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-test 53 | test 54 | 55 | 56 | org.projectlombok 57 | lombok 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-data-rest 62 | 63 | 64 | org.apache.commons 65 | commons-lang3 66 | 67 | 68 | org.bouncycastle 69 | bcpkix-jdk15on 70 | 1.60 71 | 72 | 73 | com.google.guava 74 | guava 75 | 28.1-jre 76 | 77 | 78 | com.nimbusds 79 | nimbus-jose-jwt 80 | 81 | 82 | org.greenbytes.http 83 | structured-fields 84 | 0.4 85 | 86 | 87 | com.sailpoint 88 | ietf-subject-identifiers-model 89 | 0.1.0 90 | 91 | 92 | io.bspk 93 | httpsig 94 | 0.0.4 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-maven-plugin 103 | 104 | 105 | 106 | 107 | 108 | org.eclipse.m2e 109 | lifecycle-mapping 110 | 1.0.0 111 | 112 | 113 | 114 | 115 | 116 | com.github.eirslett 117 | frontend-maven-plugin 118 | 1.8.0 119 | 120 | npm 121 | bower 122 | gulp 123 | webpack 124 | npm 125 | 126 | 127 | 128 | 129 | false 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /as/src/main/java/io/bspk/oauth/xyz/Application.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz; 2 | 3 | import java.text.ParseException; 4 | import java.util.List; 5 | 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.core.convert.converter.Converter; 10 | import org.springframework.data.mongodb.core.convert.MongoCustomConversions; 11 | import org.springframework.http.client.BufferingClientHttpRequestFactory; 12 | import org.springframework.http.client.ClientHttpRequestFactory; 13 | import org.springframework.http.client.ClientHttpRequestInterceptor; 14 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 15 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 16 | import org.springframework.web.client.RestTemplate; 17 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 18 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 19 | 20 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 21 | import com.nimbusds.jose.jwk.JWK; 22 | 23 | import io.bspk.oauth.xyz.json.JWKDeserializer; 24 | import io.bspk.oauth.xyz.json.JWKSerializer; 25 | import io.bspk.oauth.xyz.json.JWTDeserializer; 26 | import io.bspk.oauth.xyz.json.JWTSerializer; 27 | 28 | @SpringBootApplication() 29 | public class Application { 30 | 31 | public static void main(String[] args) { 32 | SpringApplication.run(Application.class, args); 33 | } 34 | 35 | @Bean 36 | public JWK clientKey() { 37 | try { 38 | return JWK.parse( 39 | "{\n" + 40 | " \"kty\": \"RSA\",\n" + 41 | " \"d\": \"m1M7uj1uZMgQqd2qwqBk07rgFzbzdCAbsfu5kvqoALv3oRdyi_UVHXDhos3DZVQ3M6mKgb30XXESykY8tpWcQOU-qx6MwtSFbo-3SNx9fBtylyQosHECGyleVP79YTE4mC0odRoUIDS90J9AcFsdVtC6M2oJ3CCL577a-lJg6eYyQoRmbjdzqMnBFJ99TCfR6wBQQbzXi1K_sN6gcqhxMmQXHWlqfT7-AJIxX9QUF0rrXMMX9fPh-HboGKs2Dqoo3ofJ2XuePpmpVDvtGy_jenXmUdpsRleqnMrEI2qkBonJQSKL4HPNpsylbQyXt2UtYrzcopCp7jL-j56kRPpQAQ\",\n" + 42 | " \"e\": \"AQAB\",\n" + 43 | " \"kid\": \"xyz-client\",\n" + 44 | " \"alg\": \"RS256\",\n" + 45 | " \"n\": \"zwCT_3bx-glbbHrheYpYpRWiY9I-nEaMRpZnRrIjCs6b_emyTkBkDDEjSysi38OC73hj1-WgxcPdKNGZyIoH3QZen1MKyyhQpLJG1-oLNLqm7pXXtdYzSdC9O3-oiyy8ykO4YUyNZrRRfPcihdQCbO_OC8Qugmg9rgNDOSqppdaNeas1ov9PxYvxqrz1-8Ha7gkD00YECXHaB05uMaUadHq-O_WIvYXicg6I5j6S44VNU65VBwu-AlynTxQdMAWP3bYxVVy6p3-7eTJokvjYTFqgDVDZ8lUXbr5yCTnRhnhJgvf3VjD_malNe8-tOqK5OSDlHTy6gD9NqdGCm-Pm3Q\"\n" + 46 | "}" 47 | ); 48 | } catch (ParseException e) { 49 | return null; 50 | } 51 | } 52 | 53 | @Bean 54 | public MongoCustomConversions mongoCustomConversions() { 55 | List> list = List.of( 56 | new JWKDeserializer(), 57 | new JWKSerializer(), 58 | new JWTDeserializer(), 59 | new JWTSerializer()); 60 | 61 | return new MongoCustomConversions(list); 62 | } 63 | 64 | @Bean 65 | public WebMvcConfigurer webMvcConfigurer() { 66 | WebMvcConfigurer configurer = new WebMvcConfigurer() { 67 | @Override 68 | public void addViewControllers(ViewControllerRegistry registry) { 69 | 70 | // map all of the front-end pages to React 71 | 72 | registry.addViewController("/device").setViewName("/"); 73 | registry.addViewController("/interact").setViewName("/"); 74 | registry.addViewController("/as").setViewName("/"); 75 | } 76 | }; 77 | 78 | return configurer; 79 | } 80 | 81 | @Bean 82 | public RestTemplate restTemplate(List interceptors) { 83 | ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(new HttpComponentsClientHttpRequestFactory()); 84 | 85 | RestTemplate restTemplate = new RestTemplate(factory); 86 | restTemplate.setInterceptors(interceptors); 87 | 88 | // set up Jackson 89 | MappingJackson2HttpMessageConverter messageConverter = restTemplate.getMessageConverters().stream() 90 | .filter(MappingJackson2HttpMessageConverter.class::isInstance) 91 | .map(MappingJackson2HttpMessageConverter.class::cast) 92 | .findFirst().orElseThrow(() -> new RuntimeException("MappingJackson2HttpMessageConverter not found")); 93 | 94 | messageConverter.getObjectMapper().setSerializationInclusion(Include.NON_NULL); 95 | 96 | return restTemplate; 97 | } 98 | 99 | // for logging incoming requests in full, for debugging 100 | /* 101 | @Bean 102 | public CommonsRequestLoggingFilter logFilter() { 103 | CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); 104 | filter.setIncludeQueryString(true); 105 | filter.setIncludePayload(true); 106 | filter.setMaxPayloadLength(10000); 107 | filter.setIncludeHeaders(true); 108 | filter.setAfterMessagePrefix("REQUEST DATA : "); 109 | return filter; 110 | } 111 | */ 112 | } 113 | -------------------------------------------------------------------------------- /rs/src/main/java/io/bspk/oauth/xyz/rs/ResourceEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.rs; 2 | 3 | import java.time.Instant; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | 9 | import org.greenbytes.http.sfv.Dictionary; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.http.HttpHeaders; 12 | import org.springframework.http.HttpMethod; 13 | import org.springframework.http.HttpStatus; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.stereotype.Controller; 17 | import org.springframework.web.bind.annotation.CrossOrigin; 18 | import org.springframework.web.bind.annotation.GetMapping; 19 | import org.springframework.web.bind.annotation.RequestHeader; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | 22 | import com.google.common.base.Strings; 23 | import com.google.common.collect.ImmutableMap; 24 | 25 | import io.bspk.oauth.xyz.crypto.SignatureVerifier; 26 | import io.bspk.oauth.xyz.data.AccessToken; 27 | import io.bspk.oauth.xyz.data.Key; 28 | import io.bspk.oauth.xyz.data.Key.Proof; 29 | import io.bspk.oauth.xyz.data.Transaction; 30 | import io.bspk.oauth.xyz.data.api.MultipleAwareField; 31 | 32 | /** 33 | * @author jricher 34 | * 35 | */ 36 | @Controller 37 | @CrossOrigin 38 | @RequestMapping("/api/rs") 39 | public class ResourceEndpoint { 40 | 41 | @Autowired 42 | private TokenRepository tokenRepository; 43 | 44 | 45 | @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) 46 | public ResponseEntity getResource( 47 | @RequestHeader(name = HttpHeaders.AUTHORIZATION, required = false) String auth, 48 | @RequestHeader(name = "Signature", required = false) Dictionary signature, 49 | @RequestHeader(name = "Signature-Input", required = false) Dictionary signatureInput, 50 | @RequestHeader(name = "Content-Digest", required = false) Dictionary contentDigest, 51 | @RequestHeader(name = "Digest", required = false) String digest, 52 | @RequestHeader(name = "Detached-JWS", required = false) String jwsd, 53 | @RequestHeader(name = "DPoP", required = false) String dpop, 54 | @RequestHeader(name = "PoP", required = false) String oauthPop, 55 | HttpServletRequest req) { 56 | 57 | String tokenValue = SignatureVerifier.extractBoundAccessToken(auth, oauthPop); 58 | 59 | if (Strings.isNullOrEmpty(tokenValue)) { 60 | return ResponseEntity.notFound().build(); 61 | } 62 | 63 | Transaction t = tokenRepository.findFirstByAccessTokenDataValue(tokenValue); 64 | 65 | if (t == null || t.getAccessToken() == null) { 66 | return ResponseEntity.badRequest().build(); 67 | } 68 | 69 | // get the specificToken 70 | MultipleAwareField tokens = t.getAccessToken(); 71 | AccessToken token = null; 72 | if (tokens.isMultiple()) { 73 | token = tokens.asMultiple().stream() 74 | .filter(v -> v.getValue().equals(tokenValue)) 75 | .findFirst() 76 | .orElse(null); 77 | } else { 78 | token = tokens.asSingle(); 79 | } 80 | 81 | // find the validation method for the token 82 | Key k = token.getKey(); 83 | if (k != null) { 84 | // if there's a key then it's not a bearer token 85 | if (k.getProof() == null) { 86 | // no proof method? shouldn't happen 87 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); 88 | } else { 89 | switch (k.getProof()) { 90 | case HTTPSIG: 91 | SignatureVerifier.ensureContentDigest(contentDigest, req); // make sure the digest header is accurate 92 | SignatureVerifier.checkHttpMessageSignature(signature, signatureInput, req, k.getJwk()); 93 | break; 94 | case JWSD: 95 | SignatureVerifier.checkDetachedJws(jwsd, req, k.getJwk(), token.getValue()); 96 | break; 97 | case DPOP: 98 | SignatureVerifier.checkDpop(dpop, req, k.getJwk(), token.getValue()); 99 | break; 100 | case OAUTHPOP: 101 | SignatureVerifier.checkOAuthPop(oauthPop, req, k.getJwk(), token.getValue()); 102 | break; 103 | case JWS: 104 | if (req.getMethod().equals(HttpMethod.GET.toString()) 105 | || req.getMethod().equals(HttpMethod.OPTIONS.toString()) 106 | || req.getMethod().equals(HttpMethod.DELETE.toString()) 107 | || req.getMethod().equals(HttpMethod.HEAD.toString()) 108 | || req.getMethod().equals(HttpMethod.TRACE.toString())) { 109 | 110 | // a body-less method was used, check the header instead 111 | SignatureVerifier.checkDetachedJws(jwsd, req, k.getJwk(), token.getValue()); 112 | } else { 113 | SignatureVerifier.checkAttachedJws(req, k.getJwk(), token.getValue()); 114 | } 115 | break; 116 | case MTLS: 117 | default: 118 | throw new RuntimeException("Unsupported Key Proof Type"); 119 | } 120 | } 121 | } 122 | 123 | // if we get here, it's either a bearer token or we have survived the proofing process for the appropriate key 124 | 125 | Map res = ImmutableMap.of( 126 | "date", Instant.now(), 127 | "overall_access", t.getAccessTokenRequest(), 128 | "token_access", token.getAccessRequest(), 129 | "proof", Optional.ofNullable(k) 130 | .map(Key::getProof) 131 | .map(Proof::name).orElse("bearer") 132 | ); 133 | 134 | 135 | return ResponseEntity.ok(res); 136 | 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /rs/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.3.4.RELEASE 10 | 11 | 12 | io.bspk 13 | oauth.xyz.rs 14 | 0.1.4-SNAPSHOT 15 | oauth.xyz-java 16 | Transactional Authorization Strawman 17 | 18 | 19 | 14 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-mongodb 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-devtools 35 | runtime 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | org.projectlombok 44 | lombok 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-data-rest 49 | 50 | 51 | org.apache.commons 52 | commons-lang3 53 | 54 | 55 | org.bouncycastle 56 | bcpkix-jdk15on 57 | 1.60 58 | 59 | 60 | com.google.guava 61 | guava 62 | 28.1-jre 63 | 64 | 65 | com.nimbusds 66 | nimbus-jose-jwt 67 | 9.4.1 68 | 69 | 70 | io.bspk 71 | oauth.xyz.lib 72 | 0.1.4-SNAPSHOT 73 | 74 | 75 | 76 | 77 | 78 | 79 | com.heroku.sdk 80 | heroku-maven-plugin 81 | 3.0.3 82 | 83 | gnap-rs 84 | 85 | java -Dserver.port=$PORT -Dspring.profiles.active=heroku $JAVA_OPTS -jar target/*.jar 86 | 87 | 88 | 89 | 90 | org.springframework.boot 91 | spring-boot-maven-plugin 92 | 93 | 94 | com.github.eirslett 95 | frontend-maven-plugin 96 | 1.11.0 97 | 98 | target 99 | 100 | 101 | 102 | install node and npm 103 | 104 | install-node-and-npm 105 | 106 | 107 | v10.16.3 108 | 6.10.3 109 | 110 | 111 | 112 | npm install 113 | 114 | npm 115 | 116 | 117 | install 118 | 119 | 120 | 121 | webpack build 122 | 123 | webpack 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | org.eclipse.m2e 133 | lifecycle-mapping 134 | 1.0.0 135 | 136 | 137 | 138 | 139 | 140 | com.github.eirslett 141 | frontend-maven-plugin 142 | 1.11.0 143 | 144 | npm 145 | bower 146 | gulp 147 | webpack 148 | npm 149 | 150 | 151 | 152 | 153 | false 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/http/DigestWrappingFilter.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.http; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.ByteArrayInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | 9 | import javax.servlet.Filter; 10 | import javax.servlet.FilterChain; 11 | import javax.servlet.ReadListener; 12 | import javax.servlet.ServletException; 13 | import javax.servlet.ServletInputStream; 14 | import javax.servlet.ServletRequest; 15 | import javax.servlet.ServletResponse; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpUpgradeHandler; 18 | 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.stereotype.Component; 22 | import org.springframework.util.Assert; 23 | 24 | import lombok.experimental.Delegate; 25 | 26 | /** 27 | * @author jricher 28 | * 29 | */ 30 | @Component 31 | public class DigestWrappingFilter implements Filter { 32 | 33 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 34 | 35 | public static final String BODY_BYTES = "BODY_BYTES"; 36 | 37 | @Override 38 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 39 | 40 | CachingRequestWrapper requestWrapper = new CachingRequestWrapper((HttpServletRequest) request); 41 | 42 | attachBodyBytes(requestWrapper); 43 | 44 | chain.doFilter(requestWrapper, response); 45 | 46 | } 47 | 48 | private void attachBodyBytes(CachingRequestWrapper requestWrapper) { 49 | byte[] bytes = requestWrapper.getSavedBody(); 50 | 51 | /* 52 | log.info(IntStream.range(0, bytes.length) 53 | .map(idx -> Byte.toUnsignedInt(bytes[idx])) 54 | .mapToObj(i -> Integer.toHexString(i)) 55 | .collect(Collectors.joining(", "))); 56 | */ 57 | 58 | requestWrapper.setAttribute(BODY_BYTES, bytes); 59 | 60 | } 61 | 62 | private class CachingRequestWrapper implements HttpServletRequest { 63 | @Delegate(types = HttpServletRequest.class, excludes = ExcludeReaderAndInputStream.class) 64 | private HttpServletRequest delegate; 65 | 66 | private ServletInputStream savedInputStream; 67 | private BufferedReader savedReader; 68 | 69 | private byte[] savedBody; 70 | 71 | public CachingRequestWrapper(HttpServletRequest delegate) throws IOException { 72 | 73 | // cache the body as a byte array 74 | this.delegate = delegate; 75 | 76 | this.savedBody = delegate.getInputStream().readAllBytes(); 77 | 78 | ByteArrayInputStream sourceStream = new ByteArrayInputStream(getSavedBody()); 79 | 80 | this.savedInputStream = new DelegatingServletInputStream(sourceStream); 81 | 82 | this.savedReader = new BufferedReader(new InputStreamReader(sourceStream)); 83 | 84 | } 85 | 86 | @Override 87 | public ServletInputStream getInputStream() throws IOException { 88 | return savedInputStream; 89 | } 90 | 91 | @Override 92 | public BufferedReader getReader() throws IOException { 93 | return savedReader; 94 | } 95 | 96 | public byte[] getSavedBody() { 97 | return savedBody; 98 | } 99 | 100 | @Override 101 | public T upgrade( 102 | Class httpUpgradeHandlerClass) throws java.io.IOException, ServletException { 103 | return delegate.upgrade(httpUpgradeHandlerClass); 104 | } 105 | 106 | } 107 | 108 | // marker interface for lombok tagging 109 | private interface ExcludeReaderAndInputStream { 110 | public ServletInputStream getInputStream(); 111 | public BufferedReader getReader(); 112 | public T upgrade( 113 | Class httpUpgradeHandlerClass) throws java.io.IOException, ServletException; 114 | } 115 | 116 | 117 | // Copied from Spring Test Tools 118 | private class DelegatingServletInputStream extends ServletInputStream { 119 | 120 | private final InputStream sourceStream; 121 | 122 | private boolean finished = false; 123 | 124 | 125 | /** 126 | * Create a DelegatingServletInputStream for the given source stream. 127 | * @param sourceStream the source stream (never {@code null}) 128 | */ 129 | public DelegatingServletInputStream(InputStream sourceStream) { 130 | Assert.notNull(sourceStream, "Source InputStream must not be null"); 131 | this.sourceStream = sourceStream; 132 | } 133 | 134 | /** 135 | * Return the underlying source stream (never {@code null}). 136 | */ 137 | public final InputStream getSourceStream() { 138 | return this.sourceStream; 139 | } 140 | 141 | 142 | @Override 143 | public int read() throws IOException { 144 | int data = this.sourceStream.read(); 145 | if (data == -1) { 146 | this.finished = true; 147 | } 148 | return data; 149 | } 150 | 151 | @Override 152 | public int available() throws IOException { 153 | return this.sourceStream.available(); 154 | } 155 | 156 | @Override 157 | public void close() throws IOException { 158 | super.close(); 159 | this.sourceStream.close(); 160 | } 161 | 162 | @Override 163 | public boolean isFinished() { 164 | return this.finished; 165 | } 166 | 167 | @Override 168 | public boolean isReady() { 169 | return true; 170 | } 171 | 172 | @Override 173 | public void setReadListener(ReadListener readListener) { 174 | throw new UnsupportedOperationException(); 175 | } 176 | 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /rc/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.3.4.RELEASE 10 | 11 | 12 | io.bspk 13 | oauth.xyz.rc 14 | 0.1.4-SNAPSHOT 15 | oauth.xyz-java 16 | Transactional Authorization Strawman 17 | 18 | 19 | 14 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-mongodb 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-devtools 35 | runtime 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | org.projectlombok 44 | lombok 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-data-rest 49 | 50 | 51 | org.apache.commons 52 | commons-lang3 53 | 54 | 55 | org.bouncycastle 56 | bcpkix-jdk15on 57 | 1.60 58 | 59 | 60 | com.google.guava 61 | guava 62 | 28.1-jre 63 | 64 | 65 | io.bspk 66 | oauth.xyz.lib 67 | 0.1.4-SNAPSHOT 68 | 69 | 70 | org.apache.httpcomponents 71 | httpclient 72 | 73 | 74 | com.nimbusds 75 | nimbus-jose-jwt 76 | 9.4.1 77 | 78 | 79 | 80 | 81 | 82 | 83 | com.heroku.sdk 84 | heroku-maven-plugin 85 | 3.0.3 86 | 87 | gnap-c 88 | 89 | java -Dserver.port=$PORT -Dspring.profiles.active=heroku $JAVA_OPTS -jar target/*.jar 90 | 91 | 92 | 93 | 94 | org.springframework.boot 95 | spring-boot-maven-plugin 96 | 97 | 98 | com.github.eirslett 99 | frontend-maven-plugin 100 | 1.11.0 101 | 102 | target 103 | 104 | 105 | 106 | install node and npm 107 | 108 | install-node-and-npm 109 | 110 | 111 | v10.16.3 112 | 6.10.3 113 | 114 | 115 | 116 | npm install 117 | 118 | npm 119 | 120 | 121 | install 122 | 123 | 124 | 125 | webpack build 126 | 127 | webpack 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | org.eclipse.m2e 137 | lifecycle-mapping 138 | 1.0.0 139 | 140 | 141 | 142 | 143 | 144 | com.github.eirslett 145 | frontend-maven-plugin 146 | 1.11.0 147 | 148 | npm 149 | bower 150 | gulp 151 | webpack 152 | npm 153 | 154 | 155 | 156 | 157 | false 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /as/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.3.4.RELEASE 10 | 11 | 12 | io.bspk 13 | oauth.xyz.as 14 | 0.1.4-SNAPSHOT 15 | oauth.xyz-java 16 | Transactional Authorization Strawman 17 | 18 | 19 | 14 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-mongodb 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-devtools 35 | runtime 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | org.projectlombok 44 | lombok 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-data-rest 49 | 50 | 51 | org.apache.commons 52 | commons-lang3 53 | 54 | 55 | org.bouncycastle 56 | bcpkix-jdk15on 57 | 1.60 58 | 59 | 60 | com.google.guava 61 | guava 62 | 28.1-jre 63 | 64 | 65 | io.bspk 66 | oauth.xyz.lib 67 | 0.1.4-SNAPSHOT 68 | 69 | 70 | org.apache.httpcomponents 71 | httpclient 72 | 4.5.13 73 | 74 | 75 | com.nimbusds 76 | nimbus-jose-jwt 77 | 9.4.1 78 | 79 | 80 | 81 | 82 | 83 | 84 | com.heroku.sdk 85 | heroku-maven-plugin 86 | 3.0.3 87 | 88 | gnap-as 89 | 90 | java -Dserver.port=$PORT -Dspring.profiles.active=heroku -agentlib:jdwp=transport=dt_socket,server=y,address=9090,suspend=n $JAVA_OPTS -jar target/*.jar 91 | 92 | 93 | 94 | 95 | org.springframework.boot 96 | spring-boot-maven-plugin 97 | 98 | 99 | com.github.eirslett 100 | frontend-maven-plugin 101 | 1.11.0 102 | 103 | target 104 | 105 | 106 | 107 | install node and npm 108 | 109 | install-node-and-npm 110 | 111 | 112 | v10.16.3 113 | 6.10.3 114 | 115 | 116 | 117 | npm install 118 | 119 | npm 120 | 121 | 122 | install 123 | 124 | 125 | 126 | webpack build 127 | 128 | webpack 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | org.eclipse.m2e 138 | lifecycle-mapping 139 | 1.0.0 140 | 141 | 142 | 143 | 144 | 145 | com.github.eirslett 146 | frontend-maven-plugin 147 | 1.11.0 148 | 149 | npm 150 | bower 151 | gulp 152 | webpack 153 | npm 154 | 155 | 156 | 157 | 158 | false 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /as/src/main/js/authserver.js: -------------------------------------------------------------------------------- 1 | // Authorization server admin page 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import http from './http'; 6 | import { Button, Badge, Row, Col, Container, Card, CardImg, CardText, CardBody, CardTitle, CardSubtitle, CardHeader, Input } from 'reactstrap'; 7 | import { BrowserRouter, Switch, Route } from 'react-router-dom'; 8 | 9 | 10 | class AuthServer extends React.Component { 11 | 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | transactions: [], 17 | clients: [] 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | document.title = "XYZ Auth Server"; 23 | this.refreshTransactions(); 24 | this.refreshClients(); 25 | } 26 | 27 | refreshTransactions = () => { 28 | return http({ 29 | method: 'GET', 30 | path: '/api/transaction/getall' 31 | }).done(response => { 32 | this.setState({ 33 | transactions: response.entity 34 | }); 35 | }); 36 | } 37 | 38 | cancelTransaction = (transactionId) => (e) => { 39 | http({ 40 | method: 'DELETE', 41 | path: '/api/transaction/' + encodeURIComponent(transactionId) 42 | }).done(response => { 43 | this.refreshTransactions(); 44 | }); 45 | 46 | } 47 | 48 | refreshClients = () => { 49 | return http({ 50 | method: 'GET', 51 | path: '/api/clients' 52 | }).done(response => { 53 | this.setState({ 54 | clients: response.entity._embedded.clients 55 | }); 56 | }); 57 | } 58 | 59 | cancelClient = (clientId) => (e) => { 60 | http({ 61 | method: 'DELETE', 62 | path: clientId 63 | }).done(response => { 64 | this.refreshClients(); 65 | }); 66 | 67 | } 68 | 69 | render() { 70 | return ( 71 | 72 | 73 | 74 | 75 | 76 | 77 | ); 78 | } 79 | 80 | } 81 | 82 | class TransactionList extends React.Component{ 83 | render() { 84 | const transactions = this.props.transactions.map( 85 | transaction => 86 | 87 | ).reverse(); // newest first 88 | return ( 89 | <> 90 |

Transactions

91 | {transactions} 92 | 93 | ); 94 | 95 | } 96 | } 97 | 98 | class Transaction extends React.Component{ 99 | render() { 100 | return ( 101 | 102 | 103 | 104 | 105 | 106 |
107 |
Status
108 |
109 | {this.props.transaction.continue_access_token && 110 | <> 111 |
Continue Token
112 |
{this.props.transaction.continue_access_token.value}
113 | 114 | } 115 | {this.props.transaction.interact && 116 | <> 117 |
Interaction URL
118 |
{this.props.transaction.interact.interaction_url}
119 |
User Code (Standalone)
120 |
{this.props.transaction.interact.standalone_user_code}
121 |
User Code
122 |
{this.props.transaction.interact.user_code}
123 |
User Code URL
124 |
{this.props.transaction.interact.user_code_url}
125 |
Callback URL
126 |
{this.props.transaction.interact.callback_uri}
127 |
Callback Method
128 |
{this.props.transaction.interact.callback_method}
129 |
Callback Hash Method
130 |
{this.props.transaction.interact.callback_hash_method}
131 |
Server Nonce
132 |
{this.props.transaction.interact.server_nonce}
133 |
Client Nonce
134 |
{this.props.transaction.interact.client_nonce}
135 | 136 | } 137 |
138 |
139 |
140 | ); 141 | } 142 | } 143 | 144 | class TransactionStatus extends React.Component { 145 | render() { 146 | switch (this.props.status) { 147 | case 'new': 148 | return ({this.props.status}); 149 | case 'issued': 150 | return ({this.props.status}); 151 | case 'authorized': 152 | return ({this.props.status}); 153 | case 'waiting': 154 | return ({this.props.status}); 155 | case 'denied': 156 | return ({this.props.status}); 157 | default: 158 | return (UNKNOWN : {this.props.status}); 159 | } 160 | } 161 | } 162 | 163 | class ClientList extends React.Component{ 164 | render() { 165 | const clients = this.props.clients.map( 166 | client => 167 | 168 | ).reverse(); // newest first 169 | return ( 170 | <> 171 |

Clients

172 | {clients} 173 | 174 | ); 175 | 176 | } 177 | } 178 | 179 | class Client extends React.Component{ 180 | render() { 181 | return ( 182 | 183 | 184 | 185 | 186 | 187 |
188 |
ID
189 |
{this.props.client._links.self.href}
190 | {this.props.client.display && 191 | <> 192 |
Display Name
193 |
{this.props.client.display.name}
194 |
Homepage
195 |
{this.props.client.display.uri}
196 | 197 | } 198 | {this.props.client.key && 199 | <> 200 |
Proof
201 |
{this.props.client.key.proof}
202 | 203 | } 204 |
205 |
206 |
207 | ); 208 | } 209 | } 210 | 211 | 212 | 213 | export default AuthServer; 214 | -------------------------------------------------------------------------------- /lib/src/main/java/io/bspk/oauth/xyz/http/JoseUnwrappingFilter.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.http; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.ByteArrayInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.InputStreamReader; 8 | import java.text.ParseException; 9 | import java.util.Collections; 10 | import java.util.Enumeration; 11 | import java.util.List; 12 | 13 | import javax.servlet.Filter; 14 | import javax.servlet.FilterChain; 15 | import javax.servlet.ReadListener; 16 | import javax.servlet.ServletException; 17 | import javax.servlet.ServletInputStream; 18 | import javax.servlet.ServletRequest; 19 | import javax.servlet.ServletResponse; 20 | import javax.servlet.http.HttpServletRequest; 21 | import javax.servlet.http.HttpUpgradeHandler; 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.springframework.stereotype.Component; 26 | import org.springframework.util.Assert; 27 | 28 | import com.nimbusds.jose.JOSEObject; 29 | 30 | import lombok.experimental.Delegate; 31 | 32 | /** 33 | * @author jricher 34 | * 35 | */ 36 | @Component 37 | public class JoseUnwrappingFilter implements Filter { 38 | 39 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 40 | 41 | public static final String BODY_JOSE = "BODY_JOSE"; 42 | 43 | @Override 44 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 45 | 46 | 47 | HttpServletRequest req = (HttpServletRequest) request; 48 | if (req.getContentType() != null && req.getContentType().equals("application/jose")) { 49 | CachingRequestWrapper requestWrapper = new CachingRequestWrapper(req); 50 | 51 | processJose(requestWrapper); 52 | 53 | chain.doFilter(requestWrapper, response); 54 | } else { 55 | // it's not a JOSE payload, ignore it 56 | chain.doFilter(request, response); 57 | } 58 | 59 | } 60 | 61 | private void processJose(CachingRequestWrapper requestWrapper) { 62 | JOSEObject jose = requestWrapper.getJose(); 63 | 64 | /* 65 | log.info(IntStream.range(0, bytes.length) 66 | .map(idx -> Byte.toUnsignedInt(bytes[idx])) 67 | .mapToObj(i -> Integer.toHexString(i)) 68 | .collect(Collectors.joining(", "))); 69 | */ 70 | 71 | // save the original JOSE item as an inbound attribute 72 | requestWrapper.setAttribute(BODY_JOSE, jose); 73 | 74 | } 75 | 76 | private class CachingRequestWrapper implements HttpServletRequest { 77 | @Delegate(types = HttpServletRequest.class, excludes = ExcludeMaskedMethods.class) 78 | private HttpServletRequest delegate; 79 | 80 | private ServletInputStream savedInputStream; 81 | private BufferedReader savedReader; 82 | 83 | private JOSEObject jose; 84 | 85 | private long length; 86 | 87 | public CachingRequestWrapper(HttpServletRequest delegate) throws IOException { 88 | 89 | try { 90 | // cache the body as a byte array 91 | this.delegate = delegate; 92 | 93 | byte[] savedBody = delegate.getInputStream().readAllBytes(); 94 | 95 | this.length = savedBody.length; 96 | 97 | this.jose = JOSEObject.parse(new String(savedBody)); 98 | 99 | // make the payload of the JWT available to the rest of the system 100 | byte[] payload = jose.getPayload().toBytes(); 101 | 102 | ByteArrayInputStream sourceStream = new ByteArrayInputStream(payload); 103 | 104 | this.savedInputStream = new DelegatingServletInputStream(sourceStream); 105 | 106 | this.savedReader = new BufferedReader(new InputStreamReader(sourceStream)); 107 | 108 | } catch (ParseException e) { 109 | throw new IOException(e); 110 | } 111 | } 112 | 113 | @Override 114 | public ServletInputStream getInputStream() throws IOException { 115 | return savedInputStream; 116 | } 117 | 118 | @Override 119 | public BufferedReader getReader() throws IOException { 120 | return savedReader; 121 | } 122 | 123 | public JOSEObject getJose() { 124 | return jose; 125 | } 126 | 127 | @Override 128 | public T upgrade( 129 | Class httpUpgradeHandlerClass) throws java.io.IOException, ServletException { 130 | return delegate.upgrade(httpUpgradeHandlerClass); 131 | } 132 | 133 | // provide the unwrapped JSON 134 | @Override 135 | public String getContentType() { 136 | return "application/json"; 137 | } 138 | 139 | @Override 140 | public int getContentLength() { 141 | return (int) this.length; 142 | } 143 | 144 | @Override 145 | public long getContentLengthLong() { 146 | return this.length; 147 | } 148 | 149 | @Override 150 | public String getHeader(String name) { 151 | if (name.equalsIgnoreCase("content-type")) { 152 | return getContentType(); 153 | } else if (name.equalsIgnoreCase("content-length")) { 154 | return Integer.toString(getContentLength()); 155 | } else { 156 | return delegate.getHeader(name); 157 | } 158 | } 159 | 160 | @Override 161 | public Enumeration getHeaders(String name) { 162 | if (name.equalsIgnoreCase("content-type")) { 163 | return Collections.enumeration(List.of(getContentType())); 164 | } else if (name.equalsIgnoreCase("content-length")) { 165 | return Collections.enumeration(List.of(Integer.toString(getContentLength()))); 166 | } else { 167 | return delegate.getHeaders(name); 168 | } 169 | } 170 | 171 | } 172 | 173 | // marker interface for lombok tagging 174 | private interface ExcludeMaskedMethods { 175 | public ServletInputStream getInputStream(); 176 | public BufferedReader getReader(); 177 | public String getContentType(); 178 | public int getContentLength(); 179 | public long getContentLengthLong(); 180 | public String getHeader(String name); 181 | public Enumeration getHeaders(String name); 182 | public T upgrade( 183 | Class httpUpgradeHandlerClass) throws java.io.IOException, ServletException; 184 | } 185 | 186 | 187 | // Copied from Spring Test Tools 188 | private class DelegatingServletInputStream extends ServletInputStream { 189 | 190 | private final InputStream sourceStream; 191 | 192 | private boolean finished = false; 193 | 194 | 195 | /** 196 | * Create a DelegatingServletInputStream for the given source stream. 197 | * @param sourceStream the source stream (never {@code null}) 198 | */ 199 | public DelegatingServletInputStream(InputStream sourceStream) { 200 | Assert.notNull(sourceStream, "Source InputStream must not be null"); 201 | this.sourceStream = sourceStream; 202 | } 203 | 204 | /** 205 | * Return the underlying source stream (never {@code null}). 206 | */ 207 | public final InputStream getSourceStream() { 208 | return this.sourceStream; 209 | } 210 | 211 | 212 | @Override 213 | public int read() throws IOException { 214 | int data = this.sourceStream.read(); 215 | if (data == -1) { 216 | this.finished = true; 217 | } 218 | return data; 219 | } 220 | 221 | @Override 222 | public int available() throws IOException { 223 | return this.sourceStream.available(); 224 | } 225 | 226 | @Override 227 | public void close() throws IOException { 228 | super.close(); 229 | this.sourceStream.close(); 230 | } 231 | 232 | @Override 233 | public boolean isFinished() { 234 | return this.finished; 235 | } 236 | 237 | @Override 238 | public boolean isReady() { 239 | return true; 240 | } 241 | 242 | @Override 243 | public void setReadListener(ReadListener readListener) { 244 | throw new UnsupportedOperationException(); 245 | } 246 | 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /rc/src/main/java/io/bspk/oauth/xyz/data/PendingTransaction.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.data; 2 | 3 | import java.net.URI; 4 | import java.time.Instant; 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.stream.Collectors; 10 | 11 | import org.bson.types.ObjectId; 12 | import org.springframework.data.annotation.Id; 13 | 14 | import com.fasterxml.jackson.databind.PropertyNamingStrategy; 15 | import com.fasterxml.jackson.databind.annotation.JsonNaming; 16 | 17 | import io.bspk.oauth.xyz.crypto.Hash.HashMethod; 18 | import io.bspk.httpsig.HttpSigAlgorithm; 19 | import io.bspk.oauth.xyz.crypto.KeyProofParameters; 20 | import io.bspk.oauth.xyz.data.api.AccessTokenRequest.TokenFlag; 21 | import io.bspk.oauth.xyz.data.api.AccessTokenResponse; 22 | import io.bspk.oauth.xyz.data.api.InteractResponse; 23 | import io.bspk.oauth.xyz.data.api.TransactionContinueRequest; 24 | import io.bspk.oauth.xyz.data.api.TransactionRequest; 25 | import io.bspk.oauth.xyz.data.api.TransactionResponse; 26 | import lombok.Data; 27 | import lombok.NonNull; 28 | import lombok.experimental.Accessors; 29 | 30 | /** 31 | * @author jricher 32 | * 33 | */ 34 | @Data 35 | @Accessors(chain = true) 36 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 37 | public class PendingTransaction { 38 | 39 | @Data 40 | @Accessors(chain = true) 41 | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 42 | public class Entry { 43 | private @Id String id = new ObjectId().toHexString(); 44 | private TransactionRequest request; 45 | private TransactionContinueRequest cont; 46 | private TransactionResponse response; 47 | } 48 | 49 | private @Id String id; 50 | private List entries = new ArrayList<>(); 51 | private String owner; 52 | private String callbackId; 53 | private String clientNonce; 54 | private String serverNonce; 55 | private HashMethod hashMethod; 56 | private KeyProofParameters proofParams; 57 | private String keyHandle; 58 | private URI continueUri; 59 | private String continueToken; 60 | private String accessToken; 61 | private KeyProofParameters accessTokenProofParams; 62 | private String rsResponse; 63 | 64 | private String standaloneUserCode; 65 | private String userCode; 66 | private URI userCodeUrl; 67 | private URI interactionUrl; 68 | private Map multipleAccessTokens; 69 | private Map multipleAccessTokenProofParams; 70 | private Map multipleRsResponse; 71 | @NonNull 72 | private final URI grantEndpoint; 73 | private Instant createdAt; 74 | private Subject subjectInfo; 75 | 76 | public PendingTransaction add (TransactionResponse response) { 77 | entries.add(new Entry().setResponse(response)); 78 | 79 | return processResponse(response); 80 | } 81 | 82 | public PendingTransaction add (TransactionContinueRequest request, TransactionResponse response) { 83 | entries.add(new Entry().setCont(request).setResponse(response)); 84 | 85 | return processResponse(response); 86 | } 87 | 88 | public PendingTransaction add (TransactionRequest request, TransactionResponse response) { 89 | entries.add(new Entry().setRequest(request).setResponse(response)); 90 | 91 | return processResponse(response); 92 | } 93 | 94 | private PendingTransaction processResponse(TransactionResponse response) { 95 | if (response.getCont() != null) { 96 | // if there's a continuation section, update the values given 97 | if (response.getCont().getAccessToken() != null) { 98 | setContinueToken(response.getCont().getAccessToken().getValue()); 99 | 100 | // set the key if an explicit one is given, otherwise keep what we have 101 | if (response.getCont().getAccessToken().getKey() != null) { 102 | KeyProofParameters params = new KeyProofParameters() 103 | .setSigningKey(response.getCont().getAccessToken().getKey().getJwk()) 104 | .setProof(response.getCont().getAccessToken().getKey().getProof()) 105 | // FIXME: these should be parameters, probably under "proof" 106 | .setDigestAlgorithm("sha-512") 107 | .setHttpSigAlgorithm(HttpSigAlgorithm.RSAPSS); 108 | 109 | 110 | setProofParams(params); 111 | } 112 | } 113 | 114 | setContinueUri(response.getCont().getUri()); 115 | } else { 116 | // otherwise clear it out 117 | setContinueToken(null); 118 | setContinueUri(null); 119 | setProofParams(null); 120 | } 121 | 122 | if (response.getAccessToken() != null) { 123 | if (response.getAccessToken().isMultiple()) { 124 | List tokenResponses = response.getAccessToken().asMultiple(); 125 | 126 | setMultipleAccessTokens(tokenResponses.stream() 127 | .collect(Collectors.toMap( 128 | t -> t.getLabel(), 129 | t -> t.getValue()))); 130 | 131 | setMultipleAccessTokenProofParams(tokenResponses.stream() 132 | .collect(Collectors.toMap( 133 | t -> t.getLabel(), 134 | t -> { 135 | if (t.getFlags() != null && !t.getFlags().contains(TokenFlag.BEARER)) { 136 | if (t.getKey() == null) { 137 | // the token is bound but there's no other key, use the client's key 138 | return getProofParams(); 139 | } else { 140 | KeyProofParameters params = new KeyProofParameters() 141 | .setSigningKey(t.getKey().getJwk()) 142 | .setProof(t.getKey().getProof()) 143 | // FIXME: these should be parameters, probably under "proof" 144 | .setDigestAlgorithm("sha-512") 145 | .setHttpSigAlgorithm(HttpSigAlgorithm.RSAPSS); 146 | return params; 147 | } 148 | } else { 149 | return null; 150 | } 151 | } 152 | ))); 153 | } else { 154 | AccessTokenResponse tokenResponse = response.getAccessToken().asSingle(); 155 | setAccessToken(tokenResponse.getValue()); 156 | if (tokenResponse.getFlags() != null && !tokenResponse.getFlags().contains(TokenFlag.BEARER)) { 157 | if (tokenResponse.getKey() != null) { 158 | KeyProofParameters params = new KeyProofParameters() 159 | .setSigningKey(tokenResponse.getKey().getJwk()) 160 | .setProof(tokenResponse.getKey().getProof()) 161 | // FIXME: these should be parameters, probably under "proof" 162 | .setDigestAlgorithm("sha-512") 163 | .setHttpSigAlgorithm(HttpSigAlgorithm.RSAPSS); 164 | setAccessTokenProofParams(params); 165 | } else { 166 | // otherwise set it to the token used on request 167 | setAccessTokenProofParams(getProofParams()); 168 | } 169 | } 170 | } 171 | } 172 | 173 | if (response.getInteract() != null) { 174 | InteractResponse interact = response.getInteract(); 175 | if (interact.getUserCode() != null) { 176 | setStandaloneUserCode(interact.getUserCode().getCode()); 177 | } 178 | if (interact.getUserCodeUri() != null) { 179 | setUserCode(interact.getUserCodeUri().getCode()); 180 | setUserCodeUrl(interact.getUserCodeUri().getUri()); 181 | } 182 | if (interact.getRedirect() != null) { 183 | setInteractionUrl(interact.getRedirect()); 184 | } 185 | 186 | if (interact.getFinish() != null) { 187 | setServerNonce(interact.getFinish()); 188 | } 189 | } else { 190 | setStandaloneUserCode(null); 191 | setUserCode(null); 192 | setUserCodeUrl(null); 193 | setInteractionUrl(null); 194 | setServerNonce(null); 195 | } 196 | 197 | if (response.getSubject() != null) { 198 | setSubjectInfo(response.getSubject()); 199 | } 200 | 201 | return this; 202 | } 203 | 204 | public PendingTransaction setMultipleRsResponse(String tokenId, String response) { 205 | Map mrr = getMultipleRsResponse(); 206 | if (mrr == null) { 207 | mrr = new HashMap<>(); 208 | } 209 | mrr.put(tokenId, response); 210 | setMultipleRsResponse(mrr); 211 | return this; 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /as/src/main/java/io/bspk/oauth/xyz/authserver/endpoint/InteractionEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.authserver.endpoint; 2 | 3 | import java.net.URI; 4 | import java.time.Instant; 5 | import java.util.Optional; 6 | 7 | import javax.servlet.http.HttpSession; 8 | 9 | import org.apache.commons.lang3.RandomStringUtils; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.http.HttpStatus; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.stereotype.Controller; 16 | import org.springframework.web.bind.annotation.GetMapping; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.PostMapping; 19 | import org.springframework.web.bind.annotation.RequestBody; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.client.RestTemplate; 22 | import org.springframework.web.util.UriComponentsBuilder; 23 | 24 | import io.bspk.oauth.xyz.authserver.data.api.ApprovalRequest; 25 | import io.bspk.oauth.xyz.authserver.data.api.ApprovalResponse; 26 | import io.bspk.oauth.xyz.authserver.data.api.PendingApproval; 27 | import io.bspk.oauth.xyz.authserver.data.api.UserInteractionFormSubmission; 28 | import io.bspk.oauth.xyz.authserver.repository.TransactionRepository; 29 | import io.bspk.oauth.xyz.crypto.Hash; 30 | import io.bspk.oauth.xyz.crypto.Hash.HashMethod; 31 | import io.bspk.oauth.xyz.data.InteractFinish.CallbackMethod; 32 | import io.bspk.oauth.xyz.data.Transaction; 33 | import io.bspk.oauth.xyz.data.Transaction.Status; 34 | import io.bspk.oauth.xyz.data.User; 35 | import io.bspk.oauth.xyz.data.api.PushbackRequest; 36 | 37 | /** 38 | * @author jricher 39 | * 40 | */ 41 | @Controller 42 | @RequestMapping("/api/as/interact") 43 | public class InteractionEndpoint { 44 | 45 | 46 | @Autowired 47 | private TransactionRepository transactionRepository; 48 | 49 | @Value("${oauth.xyz.root}") 50 | private String baseUrl; 51 | 52 | @Autowired 53 | private RestTemplate restTemplate; // for pushbacks 54 | 55 | @GetMapping("{id}") 56 | public ResponseEntity interact(@PathVariable ("id") String id, HttpSession session) { 57 | 58 | Transaction transaction = transactionRepository.findFirstByInteractInteractId(id); 59 | 60 | if (transaction != null) { 61 | 62 | // burn this interaction 63 | // transaction.getInteract().setInteractId(null); 64 | // transaction.getInteract().setInteractionUrl(null); 65 | 66 | transactionRepository.save(transaction); 67 | 68 | PendingApproval pending = new PendingApproval() 69 | .setTransaction(transaction); 70 | 71 | session.setAttribute("_pending_approval", pending); 72 | } 73 | 74 | return redirectToInteractionPage(); 75 | 76 | } 77 | 78 | 79 | @PostMapping("approve") 80 | public ResponseEntity approve(@RequestBody ApprovalRequest approve, HttpSession session) { 81 | 82 | PendingApproval pending = (PendingApproval) session.getAttribute("_pending_approval"); 83 | 84 | session.removeAttribute("_pending_approval"); 85 | 86 | if (pending != null && pending.getTransaction() != null) { 87 | // note we need to grab a fresh copy because we'll udpate it 88 | Transaction transaction = transactionRepository.findById(pending.getTransaction().getId()).orElseThrow(); 89 | 90 | // burn this interaction now that a decision has been made 91 | transaction.getInteract().setInteractId(null); 92 | transaction.getInteract().setInteractionUrl(null); 93 | 94 | if (approve.isApproved()) { 95 | transaction.setStatus(Status.AUTHORIZED); 96 | 97 | transaction.setUser(new User() 98 | .setId(session.getId()) 99 | .setEmail("user@example.com") 100 | .setPhone("555-user") 101 | .setIss(baseUrl) 102 | .setUpdatedAt(Instant.ofEpochMilli(session.getCreationTime())) 103 | ); 104 | } else { 105 | 106 | transaction.setStatus(Status.DENIED); 107 | 108 | } 109 | 110 | 111 | ApprovalResponse res = new ApprovalResponse(); 112 | 113 | if (transaction.getInteract().getCallbackMethod() != null) { 114 | // set up an interaction handle 115 | String interactRef = RandomStringUtils.randomAlphanumeric(30); 116 | transaction.getInteract().setInteractRef(interactRef); 117 | 118 | transactionRepository.save(transaction); // save the interaction reference and the state; note we have to do this before processing the callback 119 | 120 | String clientNonce = transaction.getInteract().getClientNonce(); 121 | String serverNonce = transaction.getInteract().getServerNonce(); 122 | HashMethod hashMethod = transaction.getInteract().getCallbackHashMethod(); 123 | 124 | URI txEndpoint = UriComponentsBuilder.fromHttpUrl(baseUrl) 125 | .path("/api/as/transaction") 126 | .build().toUri(); 127 | 128 | String hash = Hash.calculateInteractHash(clientNonce, 129 | serverNonce, 130 | interactRef, 131 | txEndpoint, 132 | hashMethod 133 | ); 134 | 135 | if (transaction.getInteract().getCallbackMethod().equals(CallbackMethod.REDIRECT)) { 136 | // do a redirection 137 | 138 | URI callback = transaction.getInteract().getCallbackUri(); 139 | URI callbackUri = UriComponentsBuilder.fromUri(callback) 140 | .queryParam("hash", hash) 141 | .queryParam("interact_ref", interactRef) 142 | .build().toUri(); 143 | 144 | res.setUri(callbackUri); 145 | } else { 146 | // do a push to the client 147 | 148 | PushbackRequest pushback = new PushbackRequest() 149 | .setHash(hash) 150 | .setInteractRef(interactRef); 151 | 152 | ResponseEntity response = restTemplate.postForEntity(transaction.getInteract().getCallbackUri(), pushback, Void.class); 153 | 154 | // callback handled in background 155 | res.setApproved(true); 156 | } 157 | } else { 158 | // no callback, just set it to approved 159 | transactionRepository.save(transaction); 160 | 161 | res.setApproved(true); 162 | } 163 | return ResponseEntity.ok(res); 164 | 165 | } else { 166 | return ResponseEntity.notFound().build(); 167 | } 168 | 169 | } 170 | 171 | @PostMapping(value = "/device", consumes = MediaType.APPLICATION_JSON_VALUE) 172 | public ResponseEntity processUserCode(HttpSession session, @RequestBody UserInteractionFormSubmission submit) { 173 | 174 | String userCode = submit.getUserCode(); 175 | 176 | // normalize the input code 177 | userCode = userCode.replace('l', '1'); // lowercase ell is a one 178 | userCode = userCode.toUpperCase(); // shift everything to uppercase 179 | userCode = userCode.replace('0', 'O'); // oh is zero 180 | userCode = userCode.replace('I', '1'); // aye is one 181 | userCode = userCode.replaceAll("[^123456789ABCDEFGHJKLMNOPQRSTUVWXYZ]", ""); // throw out all invalid characters 182 | 183 | userCode = userCode.substring(0, 8); 184 | 185 | Transaction transaction = transactionRepository.findFirstByInteractUserCode(userCode); 186 | 187 | if (transaction != null) { 188 | 189 | // process the code submission 190 | 191 | // TODO: add some kind of policy matching and ask the user and stuff 192 | 193 | 194 | transaction.getInteract().setUserCode(null); // burn the user code 195 | transaction.getInteract().setInteractionUrl(null); 196 | 197 | transactionRepository.save(transaction); 198 | 199 | PendingApproval pending = new PendingApproval() 200 | .setTransaction(transaction); 201 | 202 | session.setAttribute("_pending_approval", pending); 203 | 204 | return ResponseEntity.noContent().build(); 205 | 206 | } else { 207 | 208 | return ResponseEntity.notFound().build(); 209 | 210 | } 211 | 212 | 213 | 214 | 215 | } 216 | 217 | private ResponseEntity redirectToInteractionPage() { 218 | URI interactionPage = UriComponentsBuilder.fromUriString(baseUrl) 219 | .path("/interact") 220 | .build().toUri(); 221 | 222 | return ResponseEntity.status(HttpStatus.FOUND) 223 | .location(interactionPage) 224 | .build(); 225 | } 226 | 227 | 228 | @GetMapping("/pending") 229 | public ResponseEntity getPending(HttpSession session) { 230 | PendingApproval pending = (PendingApproval) session.getAttribute("_pending_approval"); 231 | 232 | return ResponseEntity.of(Optional.ofNullable(pending)); 233 | 234 | } 235 | 236 | } 237 | -------------------------------------------------------------------------------- /as/src/main/js/interact.js: -------------------------------------------------------------------------------- 1 | //Interaction Endpoint 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import http from './http'; 6 | import { Button, Badge, Row, Col, Container, Card, CardImg, CardText, CardBody, CardTitle, CardSubtitle, CardHeader, CardFooter, Input } from 'reactstrap'; 7 | import { BrowserRouter, Switch, Route } from 'react-router-dom'; 8 | 9 | 10 | class Interact extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | requireCode: props.requireCode, 16 | redirectTo: null, 17 | pending: null, 18 | rtn: false, 19 | userCode: undefined 20 | }; 21 | 22 | } 23 | 24 | setUserCode = (e) => { 25 | var userCode = e.target.value; 26 | 27 | if (!userCode) { 28 | userCode = ''; 29 | } 30 | 31 | userCode = userCode.replace(/l/, '1'); // lowercase ell is a one 32 | userCode = userCode.toUpperCase(); // shift everything to uppercase 33 | userCode = userCode.replace(/0/, 'O'); // oh is zero 34 | userCode = userCode.replace(/I/, '1'); // aye is one 35 | userCode = userCode.replaceAll(/[^123456789ABCDEFGHJKLMNOPQRSTUVWXYZ]/g, ""); // throw out all invalid characters 36 | 37 | if (userCode) { 38 | if (userCode.length > 4) { 39 | userCode = userCode.slice(0, 4) + ' - ' + userCode.slice(4, 8); 40 | } 41 | } else { 42 | userCode = undefined; 43 | } 44 | this.setState({ 45 | userCode: userCode 46 | }); 47 | } 48 | 49 | submit = () => { 50 | 51 | var data = {user_code: this.state.userCode}; 52 | 53 | var _self = this; 54 | 55 | $.ajax({ 56 | url: '/api/as/interact/device', 57 | type: 'POST', 58 | contentType: 'application/json', 59 | data: JSON.stringify(data), 60 | success: function(data, status) { 61 | _self.setState({requireCode:false}, () => { 62 | // it was submitted sucessfully, load the approval page 63 | _self.loadPending(); 64 | }); 65 | }, 66 | error: function(jqxhr, status, error) { 67 | // there was an error 68 | } 69 | }); 70 | } 71 | 72 | componentDidMount() { 73 | document.title = "XYZ Interaction"; 74 | 75 | this.loadPending(); 76 | } 77 | 78 | loadPending = () => { 79 | return http({ 80 | method: 'GET', 81 | path: '/api/as/interact/pending' 82 | }).done( 83 | response => { 84 | this.setState({ 85 | pending: response.entity, 86 | redirectTo: null, 87 | rtn: false 88 | }); 89 | }, 90 | error => { 91 | this.setState({ 92 | redirectTo: null, 93 | pending: null, 94 | rtn: false 95 | }); 96 | } 97 | ); 98 | } 99 | 100 | approve = () => { 101 | var data = {approved: true}; 102 | 103 | this.postApproval(data); 104 | } 105 | 106 | postApproval = (data) => { 107 | 108 | var _self = this; 109 | 110 | $.ajax({ 111 | url: '/api/as/interact/approve', 112 | type: 'POST', 113 | contentType: 'application/json', 114 | data: JSON.stringify(data), 115 | success: response => { 116 | if (response.uri) { 117 | // follow the redirect 118 | _self.setState({ 119 | redirectTo: response.uri, 120 | pending: null, 121 | rtn: false 122 | }); 123 | } else if (response.approved) { 124 | _self.setState({ 125 | redirectTo: null, 126 | pending: null, 127 | rtn: true 128 | }); 129 | } 130 | }, 131 | error: function(jqxhr, status, error) { 132 | // there was an error 133 | } 134 | }); 135 | } 136 | 137 | deny = () => { 138 | var data = {approved: false}; 139 | 140 | this.postApproval(data); 141 | } 142 | 143 | render() { 144 | console.log(this.state); 145 | if (this.state.redirectTo) { 146 | return ( 147 | 148 | ); 149 | } else if (this.state.rtn) { 150 | return ( 151 |
Please return to your device.
152 | ); 153 | } else if (this.state.requireCode) { 154 | return ( 155 | 156 | ); 157 | } else if (this.state.pending && this.state.pending.transaction) { 158 | return ( 159 | 160 | ); 161 | } else { 162 | return ( 163 |
There are no pending transactions, go away
164 | ); 165 | } 166 | } 167 | } 168 | 169 | class UserCodeForm extends React.Component { 170 | render() { 171 | return ( 172 | 173 | 174 | 175 | 176 | 177 | 178 | ); 179 | } 180 | } 181 | 182 | class ApprovalForm extends React.Component { 183 | render() { 184 | return ( 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | ); 200 | } 201 | } 202 | 203 | class ClientInfo extends React.Component { 204 | render() { 205 | 206 | if (this.props.display) { 207 | return ( 208 |
209 |

{this.props.display.name || "Client"}

210 | {this.props.display.uri} 211 |
212 | ); 213 | } else { 214 | return( 215 |
216 |

"Client"

217 |
218 | ); 219 | } 220 | 221 | 222 | } 223 | } 224 | 225 | class AccessRequestInfo extends React.Component { 226 | render() { 227 | 228 | if (this.props.access) { 229 | 230 | if (Array.isArray(this.props.access)) { 231 | // multiple token request 232 | const access = this.props.access.map(mt => { 233 | 234 | if (mt.access) { 235 | const st = mt.access.map(a => { 236 | if (typeof a === 'string' || a instanceof String) { 237 | // it's a reference 238 | return ( 239 |
  • {a}
  • 240 | ); 241 | } else { 242 | // it's an object, display the type 243 | console.log(a); 244 | return ( 245 |
  • {a.type}
  • 246 | ); 247 | } 248 | 249 | }); 250 | return
  • Token {mt.label}:
      {st}
  • ; 251 | } else { 252 | return null; 253 | } 254 | }); 255 | return ( 256 |
    257 | Access: 258 |
      {access}
    259 |
    260 | ); 261 | } else { 262 | // single token request 263 | if (this.props.access.access) { 264 | const access = this.props.access.access.map(a => { 265 | if (typeof a === 'string' || a instanceof String) { 266 | // it's a reference 267 | return ( 268 |
  • {a}
  • 269 | ); 270 | } else { 271 | // it's an object, display the type 272 | console.log(a); 273 | return ( 274 |
  • {a.type}
  • 275 | ); 276 | } 277 | 278 | }); 279 | return ( 280 |
    281 | Access: 282 |
      {access}
    283 |
    284 | ); 285 | } else { 286 | return null; 287 | } 288 | } 289 | } else { 290 | return null; 291 | } 292 | 293 | 294 | } 295 | } 296 | 297 | class SubjectRequestInfo extends React.Component { 298 | render() { 299 | 300 | if (this.props.subject) { 301 | if (this.props.subject.sub_id_formats) { 302 | const subj = this.props.subject.sub_id_formats.map(a => { 303 | return ( 304 |
  • {a}
  • 305 | ); 306 | }); 307 | return ( 308 |
    309 | Subject Identifiers: 310 |
      {subj}
    311 |
    312 | ); 313 | } else { 314 | return null; 315 | } 316 | } else { 317 | return null; 318 | } 319 | } 320 | } 321 | 322 | 323 | class Redirect extends React.Component { 324 | constructor( props ){ 325 | super(); 326 | this.state = { ...props }; 327 | } 328 | componentWillMount(){ 329 | window.location = this.state.uri; 330 | } 331 | render(){ 332 | return (
    Redirecting...
    ); 333 | } 334 | } 335 | 336 | 337 | export default Interact; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Bespoke Engineering, LLC and contributors. 2 | 3 | Portions copyright 2019 SecureKey 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | ``` 18 | ------------------------------------------------------------------------- 19 | Apache License 20 | Version 2.0, January 2004 21 | http://www.apache.org/licenses/ 22 | 23 | 24 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 25 | 26 | 1. Definitions. 27 | 28 | "License" shall mean the terms and conditions for use, reproduction, 29 | and distribution as defined by Sections 1 through 9 of this document. 30 | 31 | "Licensor" shall mean the copyright owner or entity authorized by 32 | the copyright owner that is granting the License. 33 | 34 | "Legal Entity" shall mean the union of the acting entity and all 35 | other entities that control, are controlled by, or are under common 36 | control with that entity. For the purposes of this definition, 37 | "control" means (i) the power, direct or indirect, to cause the 38 | direction or management of such entity, whether by contract or 39 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 40 | outstanding shares, or (iii) beneficial ownership of such entity. 41 | 42 | "You" (or "Your") shall mean an individual or Legal Entity 43 | exercising permissions granted by this License. 44 | 45 | "Source" form shall mean the preferred form for making modifications, 46 | including but not limited to software source code, documentation 47 | source, and configuration files. 48 | 49 | "Object" form shall mean any form resulting from mechanical 50 | transformation or translation of a Source form, including but 51 | not limited to compiled object code, generated documentation, 52 | and conversions to other media types. 53 | 54 | "Work" shall mean the work of authorship, whether in Source or 55 | Object form, made available under the License, as indicated by a 56 | copyright notice that is included in or attached to the work 57 | (an example is provided in the Appendix below). 58 | 59 | "Derivative Works" shall mean any work, whether in Source or Object 60 | form, that is based on (or derived from) the Work and for which the 61 | editorial revisions, annotations, elaborations, or other modifications 62 | represent, as a whole, an original work of authorship. For the purposes 63 | of this License, Derivative Works shall not include works that remain 64 | separable from, or merely link (or bind by name) to the interfaces of, 65 | the Work and Derivative Works thereof. 66 | 67 | "Contribution" shall mean any work of authorship, including 68 | the original version of the Work and any modifications or additions 69 | to that Work or Derivative Works thereof, that is intentionally 70 | submitted to Licensor for inclusion in the Work by the copyright owner 71 | or by an individual or Legal Entity authorized to submit on behalf of 72 | the copyright owner. For the purposes of this definition, "submitted" 73 | means any form of electronic, verbal, or written communication sent 74 | to the Licensor or its representatives, including but not limited to 75 | communication on electronic mailing lists, source code control systems, 76 | and issue tracking systems that are managed by, or on behalf of, the 77 | Licensor for the purpose of discussing and improving the Work, but 78 | excluding communication that is conspicuously marked or otherwise 79 | designated in writing by the copyright owner as "Not a Contribution." 80 | 81 | "Contributor" shall mean Licensor and any individual or Legal Entity 82 | on behalf of whom a Contribution has been received by Licensor and 83 | subsequently incorporated within the Work. 84 | 85 | 2. Grant of Copyright License. Subject to the terms and conditions of 86 | this License, each Contributor hereby grants to You a perpetual, 87 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 88 | copyright license to reproduce, prepare Derivative Works of, 89 | publicly display, publicly perform, sublicense, and distribute the 90 | Work and such Derivative Works in Source or Object form. 91 | 92 | 3. Grant of Patent License. Subject to the terms and conditions of 93 | this License, each Contributor hereby grants to You a perpetual, 94 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 95 | (except as stated in this section) patent license to make, have made, 96 | use, offer to sell, sell, import, and otherwise transfer the Work, 97 | where such license applies only to those patent claims licensable 98 | by such Contributor that are necessarily infringed by their 99 | Contribution(s) alone or by combination of their Contribution(s) 100 | with the Work to which such Contribution(s) was submitted. If You 101 | institute patent litigation against any entity (including a 102 | cross-claim or counterclaim in a lawsuit) alleging that the Work 103 | or a Contribution incorporated within the Work constitutes direct 104 | or contributory patent infringement, then any patent licenses 105 | granted to You under this License for that Work shall terminate 106 | as of the date such litigation is filed. 107 | 108 | 4. Redistribution. You may reproduce and distribute copies of the 109 | Work or Derivative Works thereof in any medium, with or without 110 | modifications, and in Source or Object form, provided that You 111 | meet the following conditions: 112 | 113 | (a) You must give any other recipients of the Work or 114 | Derivative Works a copy of this License; and 115 | 116 | (b) You must cause any modified files to carry prominent notices 117 | stating that You changed the files; and 118 | 119 | (c) You must retain, in the Source form of any Derivative Works 120 | that You distribute, all copyright, patent, trademark, and 121 | attribution notices from the Source form of the Work, 122 | excluding those notices that do not pertain to any part of 123 | the Derivative Works; and 124 | 125 | (d) If the Work includes a "NOTICE" text file as part of its 126 | distribution, then any Derivative Works that You distribute must 127 | include a readable copy of the attribution notices contained 128 | within such NOTICE file, excluding those notices that do not 129 | pertain to any part of the Derivative Works, in at least one 130 | of the following places: within a NOTICE text file distributed 131 | as part of the Derivative Works; within the Source form or 132 | documentation, if provided along with the Derivative Works; or, 133 | within a display generated by the Derivative Works, if and 134 | wherever such third-party notices normally appear. The contents 135 | of the NOTICE file are for informational purposes only and 136 | do not modify the License. You may add Your own attribution 137 | notices within Derivative Works that You distribute, alongside 138 | or as an addendum to the NOTICE text from the Work, provided 139 | that such additional attribution notices cannot be construed 140 | as modifying the License. 141 | 142 | You may add Your own copyright statement to Your modifications and 143 | may provide additional or different license terms and conditions 144 | for use, reproduction, or distribution of Your modifications, or 145 | for any such Derivative Works as a whole, provided Your use, 146 | reproduction, and distribution of the Work otherwise complies with 147 | the conditions stated in this License. 148 | 149 | 5. Submission of Contributions. Unless You explicitly state otherwise, 150 | any Contribution intentionally submitted for inclusion in the Work 151 | by You to the Licensor shall be under the terms and conditions of 152 | this License, without any additional terms or conditions. 153 | Notwithstanding the above, nothing herein shall supersede or modify 154 | the terms of any separate license agreement you may have executed 155 | with Licensor regarding such Contributions. 156 | 157 | 6. Trademarks. This License does not grant permission to use the trade 158 | names, trademarks, service marks, or product names of the Licensor, 159 | except as required for reasonable and customary use in describing the 160 | origin of the Work and reproducing the content of the NOTICE file. 161 | 162 | 7. Disclaimer of Warranty. Unless required by applicable law or 163 | agreed to in writing, Licensor provides the Work (and each 164 | Contributor provides its Contributions) on an "AS IS" BASIS, 165 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 166 | implied, including, without limitation, any warranties or conditions 167 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 168 | PARTICULAR PURPOSE. You are solely responsible for determining the 169 | appropriateness of using or redistributing the Work and assume any 170 | risks associated with Your exercise of permissions under this License. 171 | 172 | 8. Limitation of Liability. In no event and under no legal theory, 173 | whether in tort (including negligence), contract, or otherwise, 174 | unless required by applicable law (such as deliberate and grossly 175 | negligent acts) or agreed to in writing, shall any Contributor be 176 | liable to You for damages, including any direct, indirect, special, 177 | incidental, or consequential damages of any character arising as a 178 | result of this License or out of the use or inability to use the 179 | Work (including but not limited to damages for loss of goodwill, 180 | work stoppage, computer failure or malfunction, or any and all 181 | other commercial damages or losses), even if such Contributor 182 | has been advised of the possibility of such damages. 183 | 184 | 9. Accepting Warranty or Additional Liability. While redistributing 185 | the Work or Derivative Works thereof, You may choose to offer, 186 | and charge a fee for, acceptance of support, warranty, indemnity, 187 | or other liability obligations and/or rights consistent with this 188 | License. However, in accepting such obligations, You may act only 189 | on Your own behalf and on Your sole responsibility, not on behalf 190 | of any other Contributor, and only if You agree to indemnify, 191 | defend, and hold each Contributor harmless for any liability 192 | incurred by, or claims asserted against, such Contributor by reason 193 | of your accepting any such warranty or additional liability. 194 | 195 | END OF TERMS AND CONDITIONS 196 | ``` 197 | -------------------------------------------------------------------------------- /rc/src/main/java/io/bspk/oauth/xyz/http/SigningRestTemplateService.java: -------------------------------------------------------------------------------- 1 | package io.bspk.oauth.xyz.http; 2 | 3 | import java.io.IOException; 4 | import java.nio.charset.Charset; 5 | import java.security.MessageDigest; 6 | import java.time.Instant; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import org.apache.commons.lang3.RandomStringUtils; 11 | import org.bouncycastle.jcajce.provider.digest.SHA256; 12 | import org.bouncycastle.jcajce.provider.digest.SHA512; 13 | import org.greenbytes.http.sfv.ByteSequenceItem; 14 | import org.greenbytes.http.sfv.Dictionary; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.http.HttpMethod; 18 | import org.springframework.http.HttpRequest; 19 | import org.springframework.http.MediaType; 20 | import org.springframework.http.client.BufferingClientHttpRequestFactory; 21 | import org.springframework.http.client.ClientHttpRequestExecution; 22 | import org.springframework.http.client.ClientHttpRequestFactory; 23 | import org.springframework.http.client.ClientHttpRequestInterceptor; 24 | import org.springframework.http.client.ClientHttpResponse; 25 | import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; 26 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 27 | import org.springframework.stereotype.Service; 28 | import org.springframework.util.StreamUtils; 29 | import org.springframework.web.client.RestTemplate; 30 | 31 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 32 | import com.fasterxml.jackson.databind.JsonNode; 33 | import com.fasterxml.jackson.databind.ObjectMapper; 34 | import com.google.common.base.Joiner; 35 | import com.google.common.base.Splitter; 36 | import com.google.common.base.Strings; 37 | import com.nimbusds.jose.JOSEException; 38 | import com.nimbusds.jose.JOSEObjectType; 39 | import com.nimbusds.jose.JWSAlgorithm; 40 | import com.nimbusds.jose.JWSHeader; 41 | import com.nimbusds.jose.JWSObject; 42 | import com.nimbusds.jose.JWSSigner; 43 | import com.nimbusds.jose.Payload; 44 | import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory; 45 | import com.nimbusds.jose.jwk.JWK; 46 | 47 | import io.bspk.httpsig.ComponentProvider; 48 | import io.bspk.httpsig.HttpSign; 49 | import io.bspk.httpsig.MessageWrapper; 50 | import io.bspk.httpsig.SignatureBaseBuilder; 51 | import io.bspk.httpsig.SignatureParameters; 52 | import io.bspk.httpsig.spring.RestTemplateMessageWrapper; 53 | import io.bspk.httpsig.spring.RestTemplateRequestProvider; 54 | import io.bspk.oauth.xyz.crypto.Hash; 55 | import io.bspk.oauth.xyz.crypto.KeyProofParameters; 56 | import io.bspk.oauth.xyz.data.Key.Proof; 57 | 58 | /** 59 | * @author jricher 60 | * 61 | */ 62 | @Service 63 | public class SigningRestTemplateService { 64 | 65 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 66 | 67 | private ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(new HttpComponentsClientHttpRequestFactory()); 68 | 69 | public RestTemplate getSignerFor(KeyProofParameters params) { 70 | return getSignerFor(params, null); 71 | } 72 | 73 | public RestTemplate getSignerFor(KeyProofParameters params, String accessTokenValue) { 74 | 75 | if (params.getSigningKey() == null || params.getProof() == null) { 76 | return createRestTemplate(List.of( 77 | new AccessTokenInjectingInterceptor(null, accessTokenValue), 78 | new RequestResponseLoggingInterceptor() 79 | )); 80 | } 81 | 82 | Proof proof = params.getProof(); 83 | 84 | if (proof == null) { 85 | throw new IllegalArgumentException("Key proof must not be null."); 86 | } 87 | 88 | switch (proof) { 89 | case JWS: 90 | case JWSD: 91 | return createRestTemplate(List.of( 92 | new AccessTokenInjectingInterceptor(params, accessTokenValue), 93 | new JwsSigningInterceptor(params, accessTokenValue), 94 | new RequestResponseLoggingInterceptor() 95 | )); 96 | case HTTPSIG: 97 | return createRestTemplate(List.of( 98 | new ContentDigestInterceptor(params.getDigestAlgorithm()), 99 | new AccessTokenInjectingInterceptor(params, accessTokenValue), 100 | new HttpMessageSigningInterceptor(params, accessTokenValue), 101 | new RequestResponseLoggingInterceptor() 102 | )); 103 | case MTLS: 104 | default: 105 | return createRestTemplate(List.of( 106 | new RequestResponseLoggingInterceptor() 107 | )); 108 | } 109 | } 110 | 111 | private RestTemplate createRestTemplate(List interceptors) { 112 | RestTemplate restTemplate = new RestTemplate(factory); 113 | restTemplate.setInterceptors(interceptors); 114 | 115 | // set up Jackson 116 | MappingJackson2HttpMessageConverter messageConverter = restTemplate.getMessageConverters().stream() 117 | .filter(MappingJackson2HttpMessageConverter.class::isInstance) 118 | .map(MappingJackson2HttpMessageConverter.class::cast) 119 | .findFirst().orElseThrow(() -> new RuntimeException("MappingJackson2HttpMessageConverter not found")); 120 | 121 | messageConverter.getObjectMapper().setSerializationInclusion(Include.NON_NULL); 122 | 123 | return restTemplate; 124 | } 125 | 126 | private static class RequestResponseLoggingInterceptor implements ClientHttpRequestInterceptor { 127 | 128 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 129 | 130 | private final ObjectMapper mapper = new ObjectMapper(); 131 | 132 | @Override 133 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { 134 | logRequest(request, body); 135 | ClientHttpResponse response = execution.execute(request, body); 136 | logResponse(response); 137 | return response; 138 | } 139 | 140 | private void logRequest(HttpRequest request, byte[] body) throws IOException { 141 | log.info("<<<========================request begin================================================"); 142 | log.info("<<< URI : {}", request.getURI()); 143 | log.info("<<< Method : {}", request.getMethod()); 144 | log.info("<<< Headers : {}", request.getHeaders()); 145 | log.info("<<< Request body: {}", new String(body, "UTF-8")); 146 | if (MediaType.APPLICATION_JSON.equals(request.getHeaders().getContentType())) { 147 | // pretty print 148 | JsonNode node = mapper.readTree(body); 149 | String pretty = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(node); 150 | log.info("<<< Pretty :\n <<< : {}", Joiner.on("\n<<< : ").join(Splitter.on("\n").split(pretty))); 151 | } 152 | log.info("<<<=======================request end================================================"); 153 | } 154 | 155 | private void logResponse(ClientHttpResponse response) throws IOException { 156 | String bodyAsString = StreamUtils.copyToString(response.getBody(), Charset.defaultCharset()); 157 | 158 | log.info(">>>=========================response begin=========================================="); 159 | log.info(">>> Status code : {}", response.getStatusCode()); 160 | log.info(">>> Status text : {}", response.getStatusText()); 161 | log.info(">>> Headers : {}", response.getHeaders()); 162 | log.info(">>> Response body: {}", bodyAsString); 163 | if (MediaType.APPLICATION_JSON.equals(response.getHeaders().getContentType())) { 164 | // pretty print 165 | JsonNode node = mapper.readTree(bodyAsString); 166 | String pretty = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(node); 167 | log.info(">>> Pretty :\n >>> : {}", Joiner.on("\n>>> : ").join(Splitter.on("\n").split(pretty))); 168 | } 169 | log.info(">>>====================response end================================================="); 170 | } 171 | } 172 | 173 | private abstract class KeyAndTokenAwareInterceptor { 174 | private final String accessTokenValue; 175 | private final KeyProofParameters params; 176 | 177 | public KeyAndTokenAwareInterceptor(KeyProofParameters params, String accessTokenValue) { 178 | // Either field may be null for different request types 179 | // 180 | // | token | no token | 181 | // key | bound token | signed request | 182 | // no key | bearer token | unsigned request | 183 | // 184 | this.accessTokenValue = accessTokenValue; 185 | this.params = params; 186 | } 187 | 188 | public String getAccessTokenValue() { 189 | return accessTokenValue; 190 | } 191 | public KeyProofParameters getParams() { 192 | return params; 193 | } 194 | 195 | public boolean hasAccessToken() { 196 | return !Strings.isNullOrEmpty(accessTokenValue); 197 | } 198 | 199 | public boolean isBearerToken() { 200 | if (params == null || params.getSigningKey() == null || params.getProof() == null) { 201 | return true; 202 | } else { 203 | return false; 204 | } 205 | } 206 | } 207 | 208 | private class AccessTokenInjectingInterceptor extends KeyAndTokenAwareInterceptor implements ClientHttpRequestInterceptor { 209 | public AccessTokenInjectingInterceptor(KeyProofParameters params, String accessTokenValue) { 210 | super(params, accessTokenValue); 211 | // TODO Auto-generated constructor stub 212 | } 213 | 214 | @Override 215 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { 216 | if (!Strings.isNullOrEmpty(getAccessTokenValue())) { 217 | // if there's a token, inject it into the authorization header, otherwise just bypass 218 | if (isBearerToken()) { 219 | // RFC6750 style 220 | request.getHeaders().add("Authorization", "Bearer " + getAccessTokenValue()); 221 | } else { 222 | // GNAP style 223 | request.getHeaders().add("Authorization", "GNAP " + getAccessTokenValue()); 224 | } 225 | } 226 | return execution.execute(request, body); 227 | } 228 | } 229 | 230 | private class ContentDigestInterceptor implements ClientHttpRequestInterceptor { 231 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 232 | private String digestMethod; 233 | 234 | public ContentDigestInterceptor(String digestMethod) { 235 | if (Strings.isNullOrEmpty(digestMethod)) { 236 | digestMethod = "sha-256"; 237 | } 238 | this.digestMethod = digestMethod; 239 | } 240 | 241 | @Override 242 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { 243 | if (body != null && body.length > 0) { 244 | 245 | MessageDigest sha = null; 246 | if ("sha-512".equals(digestMethod)) { 247 | sha = new SHA512.Digest(); 248 | } else if ("sha-256".equals(digestMethod)) { 249 | sha = new SHA256.Digest(); 250 | } 251 | 252 | if (sha == null) { 253 | throw new RuntimeException("unknown digest algorithm: " + digestMethod); 254 | } 255 | 256 | byte[] digest = sha.digest(body); 257 | 258 | ByteSequenceItem seq = ByteSequenceItem.valueOf(digest); 259 | 260 | log.info("^^ Content Digest: " + seq.serialize()); 261 | 262 | Dictionary dict = Dictionary.valueOf(Map.of( 263 | digestMethod, seq 264 | )); 265 | 266 | request.getHeaders().add("Content-Digest", dict.serialize()); 267 | 268 | } 269 | 270 | return execution.execute(request, body); 271 | } 272 | } 273 | 274 | // this handles both the detached and attached versions 275 | private class JwsSigningInterceptor extends KeyAndTokenAwareInterceptor implements ClientHttpRequestInterceptor { 276 | 277 | public JwsSigningInterceptor(KeyProofParameters params, String accessTokenValue) { 278 | super(params, accessTokenValue); 279 | } 280 | 281 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 282 | 283 | @Override 284 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { 285 | 286 | // are we doing attached or not? 287 | boolean attached = (getParams().getProof() == Proof.JWS && 288 | (request.getMethod() == HttpMethod.PUT || 289 | request.getMethod() == HttpMethod.PATCH || 290 | request.getMethod() == HttpMethod.POST)); 291 | 292 | JWK clientKey = getParams().getSigningKey(); 293 | 294 | JWSHeader.Builder headerBuilder = new JWSHeader.Builder(JWSAlgorithm.parse(clientKey.getAlgorithm().getName())) 295 | .type(new JOSEObjectType("gnap-binding+jwsd")) 296 | .keyID(clientKey.getKeyID()) 297 | .customParam("htm", request.getMethod().toString()) 298 | .customParam("uri", request.getURI().toString()); 299 | 300 | // cover the access token if it exists 301 | if (hasAccessToken()) { 302 | headerBuilder.customParam("ath", Hash.SHA256_encode_url(getAccessTokenValue().getBytes()).toString()); 303 | } 304 | 305 | JWSHeader header = headerBuilder 306 | .build(); 307 | 308 | Payload payload; 309 | if (body == null || body.length == 0) { 310 | // if the body is empty, use an empty payload 311 | payload = new Payload(new byte[0]); 312 | } else if (!attached) { 313 | // if the body's not empty, the payload is a hash of the body 314 | payload = new Payload(Hash.SHA256_encode_url(body)); 315 | } else { 316 | // the body's not empty and it's an attached request, just use the body as the payload 317 | payload = new Payload(body); 318 | } 319 | 320 | //log.info(">> " + payload.toBase64URL().toString()); 321 | 322 | try { 323 | JWSSigner signer = new DefaultJWSSignerFactory().createJWSSigner(clientKey); 324 | 325 | JWSObject jwsObject = new JWSObject(header, payload); 326 | 327 | jwsObject.sign(signer); 328 | 329 | String signature = jwsObject.serialize(); 330 | 331 | if (attached) { 332 | 333 | // if we're doing attached JWS and there is a body to the request, replace the body and make the content type JOSE 334 | request.getHeaders().setContentType(new MediaType("application", "jose")); 335 | 336 | byte[] newBody = jwsObject.serialize().getBytes(); 337 | 338 | return execution.execute(request, newBody); 339 | } else { 340 | // if we're doing detached JWS or if we're doing attached JWS and there is no body, put the results in the header 341 | 342 | request.getHeaders().add("Detached-JWS", signature); 343 | } 344 | 345 | } catch (JOSEException e) { 346 | // TODO Auto-generated catch block 347 | e.printStackTrace(); 348 | } 349 | 350 | return execution.execute(request, body); 351 | 352 | } 353 | } 354 | 355 | private class HttpMessageSigningInterceptor extends KeyAndTokenAwareInterceptor implements ClientHttpRequestInterceptor { 356 | 357 | private final Logger log = LoggerFactory.getLogger(this.getClass()); 358 | 359 | public HttpMessageSigningInterceptor(KeyProofParameters params, String accessTokenValue) { 360 | super(params, accessTokenValue); 361 | } 362 | 363 | @Override 364 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { 365 | 366 | SignatureParameters sigParams = new SignatureParameters() 367 | .setCreated(Instant.now()) 368 | .setKeyid(getParams().getSigningKey().getKeyID()) 369 | .setNonce(RandomStringUtils.randomAlphanumeric(13)); 370 | 371 | if (getParams().getHttpSigAlgorithm() != null) { 372 | sigParams.setAlg(getParams().getHttpSigAlgorithm()); 373 | } 374 | 375 | sigParams.addComponentIdentifier("@target-uri"); 376 | sigParams.addComponentIdentifier("@method"); 377 | 378 | if (request.getMethod() == HttpMethod.PUT || 379 | request.getMethod() == HttpMethod.PATCH || 380 | request.getMethod() == HttpMethod.POST) { 381 | if (body != null && body.length > 0) { 382 | sigParams.addComponentIdentifier("Content-Length"); 383 | sigParams.addComponentIdentifier("Content-Type"); 384 | sigParams.addComponentIdentifier("Content-Digest"); 385 | } 386 | } 387 | 388 | if (hasAccessToken()) { 389 | sigParams.addComponentIdentifier("Authorization"); 390 | } 391 | 392 | ComponentProvider ctx = new RestTemplateRequestProvider(request); 393 | 394 | SignatureBaseBuilder baseBuilder = new SignatureBaseBuilder(sigParams, ctx); 395 | 396 | byte[] baseBytes = baseBuilder.createSignatureBase(); 397 | 398 | // holder for signed bytes 399 | 400 | HttpSign httpSign = new HttpSign(getParams().getHttpSigAlgorithm(), getParams().getSigningKey()); 401 | 402 | byte[] s = httpSign.sign(baseBytes); 403 | 404 | if (s == null) { 405 | throw new RuntimeException("Could not sign message."); 406 | } 407 | 408 | MessageWrapper wrapper = new RestTemplateMessageWrapper(request); 409 | wrapper.addSignature(sigParams, s); 410 | 411 | 412 | return execution.execute(request, body); 413 | } 414 | 415 | } 416 | } 417 | --------------------------------------------------------------------------------