diff --git a/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java b/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java index 58de82b..201b520 100644 --- a/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java +++ b/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java @@ -34,10 +34,10 @@ public class ClientBootstrapper String name = bootstrap.getKey(); Client client = bootstrap.getValue(); ClientEntity entity = new ClientEntity() - .setId(UUID.randomUUID().toString().replace("-", "").substring(0, 16)) + .setId(UUID.randomUUID().toString()) .setName(name) .setSecret(Credentials.resolve(client.secret())) - .setRedirectURI(client.redirectURI()) + .setAllowedRedirectURIs(client.redirectURI()) .setRealm(realm) .setPermissions(client.permissions()) .setAllowedGrants(client.allowedGrants()); diff --git a/src/main/java/de/tavolio/bootstrap/model/Client.java b/src/main/java/de/tavolio/bootstrap/model/Client.java index 9b7f1bf..9148fa8 100644 --- a/src/main/java/de/tavolio/bootstrap/model/Client.java +++ b/src/main/java/de/tavolio/bootstrap/model/Client.java @@ -4,10 +4,11 @@ import com.fasterxml.jackson.annotation.JsonProperty; import de.tavolio.realm.client.Grant; import de.tavolio.realm.user.Permission; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; -public record Client(@JsonProperty("secret") Credential secret, @JsonProperty("redirect-uri") String redirectURI, @JsonProperty("permissions") Set permissionList, @JsonProperty("allowed-grants") Set allowedGrants) +public record Client(@JsonProperty("secret") Credential secret, @JsonProperty("redirect-uris") Set redirectURI, @JsonProperty("permissions") Set permissionList, @JsonProperty("allowed-grants") Set allowedGrants) { public Set permissions() { diff --git a/src/main/java/de/tavolio/oidc/OidcResource.java b/src/main/java/de/tavolio/oidc/OidcResource.java index f2fe7bc..506f11c 100644 --- a/src/main/java/de/tavolio/oidc/OidcResource.java +++ b/src/main/java/de/tavolio/oidc/OidcResource.java @@ -17,6 +17,8 @@ import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.ws.rs.*; import jakarta.ws.rs.core.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.net.URI; import java.security.NoSuchAlgorithmException; @@ -26,6 +28,8 @@ import java.util.Map; @RequestScoped public class OidcResource { + private static final Logger LOG = LoggerFactory.getLogger(OidcResource.class); + @PathParam("realm-key") String realmKey; @@ -59,16 +63,20 @@ public class OidcResource @POST @Path("/auth") - public Response auth(@QueryParam("client_id") String clientId, @FormParam("email") String email, @FormParam("password") String password) + public Response auth(@QueryParam("client_id") String clientId, @QueryParam("redirect_uri") String redirectURI, @FormParam("email") String email, @FormParam("password") String password) { RealmEntity realm = realmService.requireByKey(realmKey); ClientEntity client = clientService.requireByNameAndRealm(clientId, realm); - if (client != null) + if (client.getAllowedRedirectURIs().contains(redirectURI)) { String code = authorizationService.generateBySessionCreation(realmKey, clientId, new AuthorizationCreation(email, password)); - return Response.status(302).location(URI.create(client.getRedirectURI() + "?code=" + code + "&state=d")).build(); + return Response.status(302).location(URI.create(redirectURI + "?code=" + code)).build(); + } + else + { + LOG.error("Invalid redirect uri provided: {}", redirectURI); + throw new BadRequestException(); } - throw new BadRequestException(); } @Authenticated @@ -83,6 +91,7 @@ public class OidcResource ClientEntity client = clientService.requireByNameAndRealm(identity.getPrincipal().getName(), realm); if (!client.getAllowedGrants().contains(grant)) { + LOG.error("Client {} has no permission for grant {}", client.getName(), grant); throw new ForbiddenException(); } @@ -107,6 +116,7 @@ public class OidcResource { return Grant.CLIENT_CREDENTIALS; } + LOG.error("Invalid grant {} provided", grantType); throw new RuntimeException(); } } diff --git a/src/main/java/de/tavolio/oidc/auth/AuthorizationService.java b/src/main/java/de/tavolio/oidc/auth/AuthorizationService.java index 58bf542..452185a 100644 --- a/src/main/java/de/tavolio/oidc/auth/AuthorizationService.java +++ b/src/main/java/de/tavolio/oidc/auth/AuthorizationService.java @@ -37,7 +37,6 @@ public class AuthorizationService @Transactional public String generateBySessionCreation(String realmKey, String clientId, AuthorizationCreation authorizationCreation) { - RealmEntity realm = realmService.requireByKey(realmKey); ClientEntity client = clientService.requireByNameAndRealm(clientId, realm); Optional accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, authorizationCreation.email()); diff --git a/src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java b/src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java index 0a10ba4..ec0f100 100644 --- a/src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java +++ b/src/main/java/de/tavolio/oidc/token/UserTokenGenerator.java @@ -26,6 +26,17 @@ public class UserTokenGenerator .sign(key); } + public String generateRefreshToken(String realmKey, String clientId, String upn, ZonedDateTime expiresAt, PrivateKey key, String keyId) + { + return Jwt.claims() + .upn(upn) + .claim("realm_key", realmKey) + .claim("client_id", clientId) + .expiresAt(expiresAt.toInstant()) + .issuer(issuerService.getIssuer(realmKey)).jws().keyId(keyId) + .sign(key); + } + public String generateIDToken(String realmKey, String clientId, String upn, ZonedDateTime expiresAt, PrivateKey key, String keyId) { return Jwt.claims() diff --git a/src/main/java/de/tavolio/oidc/token/UserTokenService.java b/src/main/java/de/tavolio/oidc/token/UserTokenService.java index 719edb4..2cab379 100644 --- a/src/main/java/de/tavolio/oidc/token/UserTokenService.java +++ b/src/main/java/de/tavolio/oidc/token/UserTokenService.java @@ -13,6 +13,8 @@ import io.quarkus.security.identity.SecurityIdentity; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; @@ -25,6 +27,8 @@ import java.util.UUID; @ApplicationScoped public class UserTokenService { + private static final Logger LOG = LoggerFactory.getLogger(UserTokenService.class); + @Inject CodeRepo codeRepo; @@ -37,29 +41,35 @@ public class UserTokenService @Inject RealmService realmService; - public TokenResponse getToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException + public TokenResponse getToken(String realmKey, String code) { return generateUserToken(realmKey, code); } - private TokenResponse generateUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException + private TokenResponse generateUserToken(String realmKey, String code) { String principal = identity.getPrincipal().getName(); RealmEntity realm = realmService.requireByKey(realmKey); CodeEntity entity = codeRepo.findByRealmAndId(realm, code); - if (entity != null && !ZonedDateTime.now().isAfter(entity.getExpiresAt()) && principal.equals(entity.getClient().getName())) + if (entity != null) { - KeypairEntity keypair = realm.getKeys().getFirst(); - PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair); - ZonedDateTime expiresAt = ZonedDateTime.now().plusSeconds(realm.getLifetime()); - TokenResponse response = new TokenResponse() - .setAccessToken(userTokenGenerator.generateAccessToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId())) - .setRefreshToken(UUID.randomUUID().toString()) - .setIdToken(userTokenGenerator.generateIDToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId())) - .setTokenType("Bearer") - .setExpiresAt(expiresAt.toInstant().getEpochSecond()); - codeRepo.delete(entity); - return response; + if (!ZonedDateTime.now().isAfter(entity.getExpiresAt()) && principal.equals(entity.getClient().getName())) + { + KeypairEntity keypair = realm.getKeys().getFirst(); + PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair); + ZonedDateTime expiresAt = ZonedDateTime.now().plusSeconds(realm.getLifetime()); + ZonedDateTime refreshTokenExpiresAt = ZonedDateTime.now().plusDays(3); + TokenResponse response = new TokenResponse() + .setAccessToken(userTokenGenerator.generateAccessToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId())) + .setRefreshToken(userTokenGenerator.generateRefreshToken(realm.getKey(), principal, entity.getAccount().getId(), refreshTokenExpiresAt, signingKey, keypair.getId())) + .setIdToken(userTokenGenerator.generateIDToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId())) + .setTokenType("Bearer") + .setExpiresAt(expiresAt.toInstant().getEpochSecond()); + codeRepo.delete(entity); + return response; + } + LOG.error("Code is expired or requesting client is not allowed to exchange code."); + throw new BadRequestException(); } throw new BadRequestException(); } diff --git a/src/main/java/de/tavolio/realm/client/Client.java b/src/main/java/de/tavolio/realm/client/Client.java index 26a061c..d5cad7f 100644 --- a/src/main/java/de/tavolio/realm/client/Client.java +++ b/src/main/java/de/tavolio/realm/client/Client.java @@ -2,6 +2,6 @@ package de.tavolio.realm.client; import java.util.Set; -public record Client(String id, String name, String redirectURI, Set allowedGrants) +public record Client(String id, String name, Set redirectURI, Set allowedGrants) { } diff --git a/src/main/java/de/tavolio/realm/client/ClientCreation.java b/src/main/java/de/tavolio/realm/client/ClientCreation.java index 276acb4..cc028d9 100644 --- a/src/main/java/de/tavolio/realm/client/ClientCreation.java +++ b/src/main/java/de/tavolio/realm/client/ClientCreation.java @@ -2,6 +2,6 @@ package de.tavolio.realm.client; import java.util.Set; -public record ClientCreation(String name, String secret, String redirectURI, Set allowedGrants) +public record ClientCreation(String name, String secret, Set redirectURI, Set allowedGrants) { } diff --git a/src/main/java/de/tavolio/realm/client/ClientEntity.java b/src/main/java/de/tavolio/realm/client/ClientEntity.java index 455e485..b36c490 100644 --- a/src/main/java/de/tavolio/realm/client/ClientEntity.java +++ b/src/main/java/de/tavolio/realm/client/ClientEntity.java @@ -23,9 +23,6 @@ public class ClientEntity implements RealmScoped private String secret; - @Column(name = "redirect_uri") - private String redirectURI; - @ManyToOne @JoinColumn(name = "realm_id") private RealmEntity realm; @@ -45,6 +42,11 @@ public class ClientEntity implements RealmScoped @Enumerated(EnumType.STRING) private Set allowedGrants = new HashSet<>(); + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "allowed_redirect_uri", joinColumns = @JoinColumn(name = "client_id")) + @Column(name = "redirect_uri") + private Set allowedRedirectURIs = new HashSet<>(); + public String getId() { return id; @@ -89,17 +91,6 @@ public class ClientEntity implements RealmScoped return this; } - public String getRedirectURI() - { - return redirectURI; - } - - public ClientEntity setRedirectURI(String redirectURI) - { - this.redirectURI = redirectURI; - return this; - } - public List getCodes() { return codes; @@ -132,4 +123,15 @@ public class ClientEntity implements RealmScoped this.allowedGrants = allowedGrants; return this; } + + public Set getAllowedRedirectURIs() + { + return allowedRedirectURIs; + } + + public ClientEntity setAllowedRedirectURIs(Set allowedRedirectURIs) + { + this.allowedRedirectURIs = allowedRedirectURIs; + return this; + } } diff --git a/src/main/java/de/tavolio/realm/client/ClientMapper.java b/src/main/java/de/tavolio/realm/client/ClientMapper.java index f338964..73dd7c4 100644 --- a/src/main/java/de/tavolio/realm/client/ClientMapper.java +++ b/src/main/java/de/tavolio/realm/client/ClientMapper.java @@ -11,6 +11,6 @@ public class ClientMapper public static Client map(ClientEntity client) { - return new Client(client.getId(), client.getName(), client.getRedirectURI(), client.getAllowedGrants()); + return new Client(client.getId(), client.getName(), client.getAllowedRedirectURIs(), client.getAllowedGrants()); } } diff --git a/src/main/java/de/tavolio/realm/client/ClientService.java b/src/main/java/de/tavolio/realm/client/ClientService.java index 561c781..af92bca 100644 --- a/src/main/java/de/tavolio/realm/client/ClientService.java +++ b/src/main/java/de/tavolio/realm/client/ClientService.java @@ -35,14 +35,14 @@ public class ClientService { RealmEntity realm = realmService.requireByKey(realmKey); ClientEntity entity = new ClientEntity(); - entity.setId(UUID.randomUUID().toString().replace("-", "").substring(0, 16)); + entity.setId(UUID.randomUUID().toString()); entity.setName(client.name()); if (!StringUtils.isBlank(client.secret())) { entity.setSecret(BcryptUtil.bcryptHash(client.secret())); } entity.setAllowedGrants(client.allowedGrants()); - entity.setRedirectURI(client.redirectURI()); + entity.setAllowedRedirectURIs(client.redirectURI()); entity.setRealm(realm); clientRepo.persist(entity); return ClientMapper.map(entity); @@ -67,7 +67,7 @@ public class ClientService if (existing != null) { existing.setName(client.name()); - existing.setRedirectURI(client.redirectURI()); + existing.setAllowedRedirectURIs(client.redirectURI()); existing.setAllowedGrants(client.allowedGrants()); clientRepo.persist(existing); return ClientMapper.map(existing); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 02cab90..2178507 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,7 +9,7 @@ quarkus.http.cors.enabled=true # Postgres prod.quarkus.hibernate-orm.validate-in-dev-mode=false quarkus.hibernate-orm.schema-management.strategy=none -%test,dev.quarkus.hibernate-orm.schema-management.strategy=drop-and-create +%test,dev.quarkus.hibernate-orm.schema-management.strategy=none quarkus.datasource.db-kind=postgresql %dev,test.quarkus.datasource.username=postgres @@ -17,14 +17,11 @@ quarkus.datasource.db-kind=postgresql %dev,test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=auth # Flyway -quarkus.flyway.enabled=false -%test.quarkus.flyway.clean-at-start=true -%dev.quarkus.flyway.clean-at-start=true +quarkus.flyway.enabled=true +quarkus.flyway.clean-at-start=true %dev.quarkus.flyway.locations=db/migration,db/dev -%test,dev.quarkus.flyway.migrate-at-start=false quarkus.flyway.migrate-at-start=true -quarkus.http.access-log.enabled=true quarkus.http.auth.basic=false %dev.io.verifoo.http.origin=http://localhost:8089 diff --git a/src/main/resources/bootstrap.yaml b/src/main/resources/bootstrap.yaml index 8c0c5c2..b7ef9e0 100644 --- a/src/main/resources/bootstrap.yaml +++ b/src/main/resources/bootstrap.yaml @@ -15,11 +15,13 @@ realms: backend: secret: bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX. - redirect-uri: http://localhost:8080/auth/callback + redirect-uris: + - http://localhost:8080/auth/callback permissions: - USER_VIEW allowed-grants: - AUTHORIZATION_CODE + - CLIENT_CREDENTIALS analytics-backend: secret: bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX. diff --git a/src/main/resources/db/dev/V9999__init.sql b/src/main/resources/db/dev/V9999__init.sql deleted file mode 100755 index a1a13e0..0000000 --- a/src/main/resources/db/dev/V9999__init.sql +++ /dev/null @@ -1,2 +0,0 @@ -INSERT INTO account (id, firstname, lastname, email, account_password, status) -VALUES ('66b261fe-4c5a-4728-9857-67717f02d4e1', 'Andreas', 'Dinauer', 'andreas.j.dinauer@gmail.com', '$2a$12$cdrzIY4sMFAXiz29uo9Ul.MPy0RN0FGS2yjVzb5BTe6bSijn4eGQy', 'REGISTERED'); \ No newline at end of file diff --git a/src/main/resources/db/migration/V1.0.1__init.sql b/src/main/resources/db/migration/V1.0.1__init.sql index 1fff205..c6da8b3 100755 --- a/src/main/resources/db/migration/V1.0.1__init.sql +++ b/src/main/resources/db/migration/V1.0.1__init.sql @@ -1,10 +1,168 @@ -CREATE TABLE account ( - email VARCHAR(255) NULL, - firstname VARCHAR(255) NULL, - id VARCHAR(255) NOT NULL, - lastname VARCHAR(255) NULL, - account_password VARCHAR(255) NULL, - status VARCHAR(255) NULL, - CONSTRAINT account_pkey PRIMARY KEY (id), - CONSTRAINT account_status_check CHECK (((status)::text = ANY ((ARRAY['INIT'::character varying, 'REGISTERED'::character varying])::text[]))) -); \ No newline at end of file +create table realm +( + lifetime integer not null, + id varchar(255) not null primary key, + key varchar(255), + realm_name varchar(255) +); + +alter table realm + owner to postgres; + +create table audience_strategy +( + id varchar(255) not null primary key, + realm_id varchar(255) unique + constraint fkth1885jeywykkufsp36n5tc56 references realm, + strategy varchar(255), + value varchar(255) +); + +alter table audience_strategy + owner to postgres; + +create table client +( + id varchar(255) not null primary key, + name varchar(255), + realm_id varchar(255) + constraint fk1bi8c7s4u3s44vw3w8le6xau1 references realm on delete cascade, + secret varchar(255) +); + +create table allowed_grants +( + client_id varchar(255) not null + constraint fkpuqnyqud9ft1tn18qrcy7457c references client, + grant_name varchar(255) + constraint allowed_grants_grant_name_check check ((grant_name)::text = ANY ((ARRAY ['CLIENT_CREDENTIALS':: character varying, 'AUTHORIZATION_CODE':: character varying, 'PASSWORD':: character varying])::text[]) +) + ); + +alter table allowed_grants + owner to postgres; + +create table allowed_redirect_uri +( + client_id varchar(255) not null + constraint fkamn8o2599ky2nactoc7umiltg references client, + redirect_uri varchar(255) +); + +alter table allowed_redirect_uri + owner to postgres; + +create table client_permission +( + client_id varchar(255) not null + constraint fk1kqfe7d8w5bor8vp0aoaloptt + references client, + permission varchar(255) + constraint client_permission_permission_check + check ((permission)::text = ANY + ((ARRAY ['USER_VIEW':: character varying, 'USER_DELETE':: character varying, 'USER_CREATE':: character varying])::text[]) +) + ); + +alter table client_permission + owner to postgres; + +create table keypair +( + alg varchar(255), + crv varchar(255), + id varchar(255) not null + primary key, + realm_id varchar(255) + constraint fk1ceq9els4s8jqs96xnmi2y3bi + references realm + on delete cascade, + type varchar(255), + use varchar(255), + x varchar(255), + y varchar(255), + privatekey bytea +); + +alter table keypair + owner to postgres; + +create table role +( + id varchar(255) not null + primary key, + realm_id varchar(255) + constraint fkocsid53ns4d9sngf84ab78bsr + references realm + on delete cascade, + role_name varchar(255) +); + +alter table role + owner to postgres; + +create table user_regular +( + account_password varchar(255), + email varchar(255), + firstname varchar(255), + id varchar(255) not null + primary key, + lastname varchar(255), + realm_id varchar(255) + constraint fki4jjeaqdf1op17hosriwn55t2 + references realm + on delete cascade, + status varchar(255) + constraint user_regular_status_check + check ((status)::text = ANY ((ARRAY ['INIT':: character varying, 'REGISTERED':: character varying])::text[]) +) + ); + +alter table user_regular + owner to postgres; + +create table assignment +( + id varchar(255) not null + primary key, + role_id varchar(255) + constraint fk90vsrx78n7k7tjr42g7xx6hmy + references role, + user_id varchar(255) + constraint fk3gqvh1h60fkikoc5ix1hvaovl + references user_regular +); + +alter table assignment + owner to postgres; + +create table code +( + expiresat timestamp(6) with time zone, + account_id varchar(255) + constraint fk7kkfrwxn8bry4boaorheikjcu + references user_regular, + client_id varchar(255) + constraint fkgs7a5cpyl2slw4j5ageba4hlv + references client, + id varchar(255) not null + primary key, + realm_id varchar(255) + constraint fkqp7vcsghxg1dag4t89ys7mmfq + references realm + on delete cascade +); + +alter table code + owner to postgres; + +create table user_super +( + id varchar(255) not null + primary key, + password varchar(255) +); + +alter table user_super + owner to postgres; \ No newline at end of file