Browse Source

implemented main part of authorization and token delivery

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 4 months ago
parent
commit
1e8ca6dc3a
  1. 7
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Authorization.java
  2. 8
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizationService.java
  3. 7
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Client.java
  4. 3
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/ClientService.java
  5. 6
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java
  6. 4
      de.srsoftware.oidc.app/build.gradle
  7. 34
      de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java
  8. 39
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java
  9. 83
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java
  10. 104
      de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java
  11. 5
      de.srsoftware.oidc.web/src/main/resources/en/authorization.html
  12. 4
      de.srsoftware.oidc.web/src/main/resources/en/authorization.js
  13. 10
      de.srsoftware.oidc.web/src/main/resources/en/edit_client.html
  14. 54
      de.srsoftware.oidc.web/src/main/resources/en/edit_client.js
  15. 2
      de.srsoftware.oidc.web/src/main/resources/en/new_client.html
  16. 0
      de.srsoftware.oidc.web/src/main/resources/en/new_client.js
  17. 19
      de.srsoftware.utils/build.gradle
  18. 13
      de.srsoftware.utils/src/main/java/de/srsoftware/utils/Optionals.java
  19. 19
      de.srsoftware.utils/src/main/java/de/srsoftware/utils/Paths.java
  20. 1
      settings.gradle

7
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Authorization.java

@ -0,0 +1,7 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.api;
import java.time.Instant;
public record Authorization(String clientId, String userId, Instant expiration) {
}

8
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/AuthorizationService.java

@ -1,13 +1,17 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.oidc.api; package de.srsoftware.oidc.api;
import java.util.Date; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface AuthorizationService { public interface AuthorizationService {
AuthorizationService authorize(Client client, User user, Date expiration); AuthorizationService addCode(Client client, User user, String code);
AuthorizationService authorize(Client client, User user, Instant expiration);
boolean isAuthorized(Client client, User user); boolean isAuthorized(Client client, User user);
List<User> authorizedUsers(Client client); List<User> authorizedUsers(Client client);
List<Client> authorizedClients(User user); List<Client> authorizedClients(User user);
AuthorizationService revoke(Client client, User user); AuthorizationService revoke(Client client, User user);
Optional<Authorization> forCode(String code);
} }

7
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Client.java

@ -2,20 +2,13 @@
package de.srsoftware.oidc.api; package de.srsoftware.oidc.api;
import static de.srsoftware.oidc.api.Constants.*; import static de.srsoftware.oidc.api.Constants.*;
import static java.lang.System.Logger.Level.WARNING;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
public record Client(String id, String name, String secret, Set<String> redirectUris) { public record Client(String id, String name, String secret, Set<String> redirectUris) {
private static System.Logger LOG = System.getLogger(Client.class.getSimpleName()); private static System.Logger LOG = System.getLogger(Client.class.getSimpleName());
public Map<String, Object> map() { public Map<String, Object> map() {
return Map.of(CLIENT_ID, id, NAME, name, SECRET, secret, REDIRECT_URIS, redirectUris); return Map.of(CLIENT_ID, id, NAME, name, SECRET, secret, REDIRECT_URIS, redirectUris);
} }
public String generateCode() {
LOG.log(WARNING, "{0}.generateCode() not implemented!", getClass().getSimpleName());
return UUID.randomUUID().toString();
}
} }

3
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/ClientService.java

@ -6,8 +6,7 @@ import java.util.Optional;
public interface ClientService { public interface ClientService {
Optional<Client> getClient(String clientId); Optional<Client> getClient(String clientId);
ClientService add(Client client);
List<Client> listClients(); List<Client> listClients();
ClientService remove(Client client); ClientService remove(Client client);
ClientService update(Client client); ClientService save(Client client);
} }

6
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java

@ -3,14 +3,16 @@ package de.srsoftware.oidc.api;
public class Constants { public class Constants {
public static final String ACCESS_TOKEN = "access_token"; public static final String ACCESS_TOKEN = "access_token";
public static final String ATUH_CODE = "authorization_code"; public static final String APP_NAME = "LightOIDC";
public static final String AUTH_CODE = "authorization_code";
public static final String BEARER = "Bearer"; public static final String BEARER = "Bearer";
public static final String CAUSE = "cause"; public static final String CAUSE = "cause";
public static final String CLIENT_ID = "client_id"; public static final String CLIENT_ID = "client_id";
public static final String CLIENT_SECRET = "client_secret"; public static final String CLIENT_SECRET = "client_secret";
public static final String CODE = "code"; public static final String CODE = "code";
public static final String CONFIG_PATH = "LIGHTOIDC_CONFIG_PATH";
public static final String CONFIRMED = "confirmed"; public static final String CONFIRMED = "confirmed";
public static final String DEFAULT_KEY = "default_key"; public static final String DAYS = "days";
public static final String EXPIRES_IN = "expires_in"; public static final String EXPIRES_IN = "expires_in";
public static final String GRANT_TYPE = "grant_type"; public static final String GRANT_TYPE = "grant_type";
public static final String ID_TOKEN = "id_token"; public static final String ID_TOKEN = "id_token";

4
de.srsoftware.oidc.app/build.gradle

@ -16,8 +16,8 @@ dependencies {
implementation project(':de.srsoftware.oidc.api') implementation project(':de.srsoftware.oidc.api')
implementation project(':de.srsoftware.oidc.backend') implementation project(':de.srsoftware.oidc.backend')
implementation project(':de.srsoftware.oidc.web') implementation project(':de.srsoftware.oidc.web')
implementation project(':de.srsoftware.oidc.datastore.file') implementation project(':de.srsoftware.utils')
} implementation project(':de.srsoftware.oidc.datastore.file')}
test { test {
useJUnitPlatform() useJUnitPlatform()

34
de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java

@ -2,9 +2,13 @@
package de.srsoftware.oidc.app; package de.srsoftware.oidc.app;
import static de.srsoftware.oidc.api.Constants.*;
import static de.srsoftware.oidc.api.Permission.MANAGE_CLIENTS; import static de.srsoftware.oidc.api.Permission.MANAGE_CLIENTS;
import static de.srsoftware.utils.Optionals.nonEmpty;
import static de.srsoftware.utils.Paths.configDir;
import static java.lang.System.Logger.Level.DEBUG; import static java.lang.System.Logger.Level.DEBUG;
import static java.lang.System.Logger.Level.ERROR; import static java.lang.System.Logger.Level.ERROR;
import static java.lang.System.getenv;
import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpServer;
import de.srsoftware.logging.ColorLogger; import de.srsoftware.logging.ColorLogger;
@ -17,7 +21,6 @@ import de.srsoftware.oidc.datastore.file.FileStore;
import de.srsoftware.oidc.datastore.file.UuidHasher; import de.srsoftware.oidc.datastore.file.UuidHasher;
import de.srsoftware.oidc.web.Forward; import de.srsoftware.oidc.web.Forward;
import de.srsoftware.oidc.web.StaticPages; import de.srsoftware.oidc.web.StaticPages;
import java.io.File;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
@ -40,19 +43,19 @@ public class Application {
private static System.Logger LOG = new ColorLogger("Application").setLogLevel(DEBUG); private static System.Logger LOG = new ColorLogger("Application").setLogLevel(DEBUG);
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
var argMap = map(args); var argMap = map(args);
Optional<Path> basePath = argMap.get(BASE_PATH) instanceof Path p ? Optional.of(p) : Optional.empty(); Optional<Path> basePath = argMap.get(BASE_PATH) instanceof Path p ? Optional.of(p) : Optional.empty();
var storageFile = new File("/tmp/lightoidc.json"); var storageFile = (argMap.get(CONFIG_PATH) instanceof Path p ? p : configDir(APP_NAME).resolve("config.json")).toFile();
var passwordHasher = new UuidHasher(); var passwordHasher = new UuidHasher();
var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID); var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID);
var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID).add(MANAGE_CLIENTS); var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID).add(MANAGE_CLIENTS);
FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser); FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser);
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server); new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server);
new Forward(INDEX).bindPath(ROOT).on(server); new Forward(INDEX).bindPath(ROOT).on(server);
new WellKnownController().bindPath(WELL_KNOWN).on(server); new WellKnownController().bindPath(WELL_KNOWN).on(server);
new UserController(fileStore, fileStore).bindPath(API_USER).on(server); new UserController(fileStore, fileStore).bindPath(API_USER).on(server);
new TokenController(fileStore).bindPath(API_TOKEN).on(server); new TokenController(fileStore, fileStore, fileStore).bindPath(API_TOKEN).on(server);
new ClientController(fileStore, fileStore, fileStore).bindPath(API_CLIENT).on(server); new ClientController(fileStore, fileStore, fileStore).bindPath(API_CLIENT).on(server);
// server.setExecutor(Executors.newCachedThreadPool()); // server.setExecutor(Executors.newCachedThreadPool());
server.setExecutor(Executors.newSingleThreadExecutor()); server.setExecutor(Executors.newSingleThreadExecutor());
@ -62,13 +65,22 @@ public class Application {
private static Map<String, Object> map(String[] args) { private static Map<String, Object> map(String[] args) {
var tokens = new ArrayList<>(List.of(args)); var tokens = new ArrayList<>(List.of(args));
var map = new HashMap<String, Object>(); var map = new HashMap<String, Object>();
nonEmpty(getenv(BASE_PATH)).map(Path::of).ifPresent(path -> map.put(BASE_PATH, path));
nonEmpty(getenv(CONFIG_PATH)).map(Path::of).ifPresent(path -> map.put(CONFIG_PATH, path));
// Command line arguments override environment
while (!tokens.isEmpty()) { while (!tokens.isEmpty()) {
var token = tokens.remove(0); var token = tokens.remove(0);
switch (token) { switch (token) {
case "--base": case "--base":
if (tokens.isEmpty()) throw new IllegalArgumentException("--path option requires second argument!"); if (tokens.isEmpty()) throw new IllegalArgumentException("--base option requires second argument!");
map.put(BASE_PATH, Path.of(tokens.remove(0))); map.put(BASE_PATH, Path.of(tokens.remove(0)));
break; break;
case "--config":
if (tokens.isEmpty()) throw new IllegalArgumentException("--config option requires second argument!");
map.put(CONFIG_PATH, Path.of(tokens.remove(0)));
break;
default: default:
LOG.log(ERROR, "Unknown option: {0}", token); LOG.log(ERROR, "Unknown option: {0}", token);
} }

39
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/ClientController.java

@ -9,8 +9,11 @@ import static java.net.HttpURLConnection.*;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.oidc.api.*; import de.srsoftware.oidc.api.*;
import java.io.IOException; import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import org.json.JSONObject; import org.json.JSONObject;
public class ClientController extends Controller { public class ClientController extends Controller {
@ -24,17 +27,6 @@ public class ClientController extends Controller {
clients = clientService; clients = clientService;
} }
private boolean add(HttpExchange ex, Session session) throws IOException {
if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED");
var json = json(ex);
var redirects = new HashSet<String>();
for (Object o : json.getJSONArray(REDIRECT_URIS)) {
if (o instanceof String s) redirects.add(s);
}
var client = new Client(json.getString(CLIENT_ID), json.getString(NAME), json.getString(SECRET), redirects);
clients.add(client);
return sendContent(ex, client);
}
private boolean authorize(HttpExchange ex, Session session) throws IOException { private boolean authorize(HttpExchange ex, Session session) throws IOException {
var user = session.user(); var user = session.user();
@ -48,14 +40,17 @@ public class ClientController extends Controller {
if (!client.redirectUris().contains(redirect)) return badRequest(ex, Map.of(CAUSE, "unknown redirect uri", REDIRECT_URI, redirect)); if (!client.redirectUris().contains(redirect)) return badRequest(ex, Map.of(CAUSE, "unknown redirect uri", REDIRECT_URI, redirect));
if (!authorizations.isAuthorized(client, session.user())) { if (!authorizations.isAuthorized(client, session.user())) {
if (json.has(CONFIRMED) && json.getBoolean(CONFIRMED)) { if (json.has(DAYS)) {
authorizations.authorize(client, user, null); var days = json.getInt(DAYS);
var expiration = Instant.now().plus(Duration.ofDays(days));
authorizations.authorize(client, user, expiration);
} else { } else {
return sendContent(ex, Map.of(CONFIRMED, false, NAME, client.name())); return sendContent(ex, Map.of(CONFIRMED, false, NAME, client.name()));
} }
} }
var state = json.getString(STATE); var state = json.getString(STATE);
var code = client.generateCode(); var code = UUID.randomUUID().toString();
authorizations.addCode(client, session.user(), code);
return sendContent(ex, Map.of(CONFIRMED, true, CODE, code, REDIRECT_URI, redirect, STATE, state)); return sendContent(ex, Map.of(CONFIRMED, true, CODE, code, REDIRECT_URI, redirect, STATE, state));
} }
@ -94,8 +89,8 @@ public class ClientController extends Controller {
switch (path) { switch (path) {
case "/": case "/":
return load(ex, session); return load(ex, session);
case "/add": case "/add", "/update":
return add(ex, session); return save(ex, session);
case "/authorize": case "/authorize":
return authorize(ex, session); return authorize(ex, session);
case "/list": case "/list":
@ -123,4 +118,16 @@ public class ClientController extends Controller {
} }
return sendEmptyResponse(HTTP_NOT_FOUND, ex); return sendEmptyResponse(HTTP_NOT_FOUND, ex);
} }
private boolean save(HttpExchange ex, Session session) throws IOException {
if (!session.user().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED");
var json = json(ex);
var redirects = new HashSet<String>();
for (Object o : json.getJSONArray(REDIRECT_URIS)) {
if (o instanceof String s) redirects.add(s);
}
var client = new Client(json.getString(CLIENT_ID), json.getString(NAME), json.getString(SECRET), redirects);
clients.save(client);
return sendContent(ex, client);
}
} }

83
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java

@ -6,10 +6,9 @@ import static java.lang.System.Logger.Level.*;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.oidc.api.Client; import de.srsoftware.oidc.api.*;
import de.srsoftware.oidc.api.ClientService;
import de.srsoftware.oidc.api.PathHandler;
import java.io.IOException; import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -21,10 +20,14 @@ import org.jose4j.lang.JoseException;
import org.json.JSONObject; import org.json.JSONObject;
public class TokenController extends PathHandler { public class TokenController extends PathHandler {
private final ClientService clients; private final ClientService clients;
private final AuthorizationService authorizations;
private final UserService users;
public TokenController(ClientService clientService) { public TokenController(AuthorizationService authorizationService, ClientService clientService, UserService userService) {
clients = clientService; authorizations = authorizationService;
clients = clientService;
users = userService;
} }
private Map<String, String> deserialize(String body) { private Map<String, String> deserialize(String body) {
@ -42,27 +45,31 @@ public class TokenController extends PathHandler {
} }
private boolean provideToken(HttpExchange ex) throws IOException { private boolean provideToken(HttpExchange ex) throws IOException {
var map = deserialize(body(ex)); var map = deserialize(body(ex));
// TODO: check Authorization Code, → https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
// TODO: check Redirect URL
LOG.log(DEBUG, "post data: {0}", map);
LOG.log(WARNING, "{0}.provideToken(ex) not implemented!", getClass().getSimpleName());
var grantType = map.get(GRANT_TYPE); var grantType = map.get(GRANT_TYPE);
if (!ATUH_CODE.equals(grantType)) sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown grant type", GRANT_TYPE, grantType)); if (!AUTH_CODE.equals(grantType)) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown grant type", GRANT_TYPE, grantType));
var optClient = Optional.ofNullable(map.get(CLIENT_ID)).flatMap(clients::getClient);
if (optClient.isEmpty()) { var code = map.get(CODE);
LOG.log(ERROR, "client not found"); var optAuthorization = authorizations.forCode(code);
return sendEmptyResponse(HTTP_BAD_REQUEST, ex); if (optAuthorization.isEmpty()) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "invalid auth code", CODE, code));
// TODO: send correct response var authorization = optAuthorization.get();
}
var clientId = map.get(CLIENT_ID);
if (!authorization.clientId().equals(clientId)) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "invalid client id", CLIENT_ID, clientId));
var optClient = clients.getClient(clientId);
if (optClient.isEmpty()) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown client", CLIENT_ID, clientId));
var client = optClient.get();
var user = users.load(authorization.userId());
if (user.isEmpty()) return sendContent(ex, 500, Map.of(ERROR, "User not found"));
var uri = URLDecoder.decode(map.get(REDIRECT_URI), StandardCharsets.UTF_8);
if (!client.redirectUris().contains(uri)) sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "unknown redirect uri", REDIRECT_URI, uri));
var secretFromClient = map.get(CLIENT_SECRET); var secretFromClient = map.get(CLIENT_SECRET);
var client = optClient.get(); if (!client.secret().equals(secretFromClient)) return sendContent(ex, HTTP_BAD_REQUEST, Map.of(ERROR, "client secret mismatch"));
if (!client.secret().equals(secretFromClient)) {
LOG.log(ERROR, "client secret mismatch"); String jwToken = createJWT(client, user.get());
return sendEmptyResponse(HTTP_BAD_REQUEST, ex);
// TODO: send correct response
}
String jwToken = createJWT(client);
ex.getResponseHeaders().add("Cache-Control", "no-store"); ex.getResponseHeaders().add("Cache-Control", "no-store");
JSONObject response = new JSONObject(); JSONObject response = new JSONObject();
response.put(ACCESS_TOKEN, UUID.randomUUID().toString()); // TODO: wofür genau wird der verwendet, was gilt es hier zu beachten response.put(ACCESS_TOKEN, UUID.randomUUID().toString()); // TODO: wofür genau wird der verwendet, was gilt es hier zu beachten
@ -72,22 +79,12 @@ public class TokenController extends PathHandler {
return sendContent(ex, response); return sendContent(ex, response);
} }
private String createJWT(Client client) { private String createJWT(Client client, User user) {
try { try {
byte[] secretBytes = client.secret().getBytes(StandardCharsets.UTF_8); byte[] secretBytes = client.secret().getBytes(StandardCharsets.UTF_8);
HmacKey hmacKey = new HmacKey(secretBytes); HmacKey hmacKey = new HmacKey(secretBytes);
JwtClaims claims = new JwtClaims(); JwtClaims claims = getJwtClaims(user);
claims.setIssuer("Issuer"); // who creates the token and signs it
claims.setAudience("Audience"); // to whom the token is intended to be sent
claims.setExpirationTimeMinutesInTheFuture(10); // time when the token will expire (10 minutes from now)
claims.setGeneratedJwtId(); // a unique identifier for the token
claims.setIssuedAtToNow(); // when the token was issued/created (now)
claims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago)
claims.setSubject("subject"); // the subject/principal is whom the token is about
claims.setClaim("email", "mail@example.com"); // additional claims/attributes about the subject can be added
List<String> groups = Arrays.asList("group-one", "other-group", "group-three");
claims.setStringListClaim("groups", groups); // multi-valued claims work too and will end up as a JSON array
// A JWT is a JWS and/or a JWE with JSON claims as the payload. // A JWT is a JWS and/or a JWE with JSON claims as the payload.
// In this example it is a JWS so we create a JsonWebSignature object. // In this example it is a JWS so we create a JsonWebSignature object.
@ -105,4 +102,16 @@ public class TokenController extends PathHandler {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private static JwtClaims getJwtClaims(User user) {
JwtClaims claims = new JwtClaims();
claims.setIssuer(APP_NAME); // who creates the token and signs it
claims.setExpirationTimeMinutesInTheFuture(10); // time when the token will expire (10 minutes from now)
claims.setGeneratedJwtId(); // a unique identifier for the token
claims.setIssuedAtToNow(); // when the token was issued/created (now)
claims.setNotBeforeMinutesInThePast(2); // time before which the token is not yet valid (2 minutes ago)
claims.setSubject(user.uuid()); // the subject/principal is whom the token is about
claims.setClaim("email", user.email()); // additional claims/attributes about the subject can be added
return claims;
}
} }

104
de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java

@ -15,14 +15,16 @@ import java.util.*;
import org.json.JSONObject; import org.json.JSONObject;
public class FileStore implements AuthorizationService, ClientService, SessionService, UserService { public class FileStore implements AuthorizationService, ClientService, SessionService, UserService {
private static final String CLIENTS = "clients"; private static final String AUTHORIZATIONS = "authorizations";
private static final String EXPIRATION = "expiration"; private static final String CLIENTS = "clients";
private static final String NAME = "name"; private static final String CODES = "codes";
private static final String REDIRECT_URIS = "redirect_uris"; private static final String EXPIRATION = "expiration";
private static final String SECRET = "secret"; private static final String NAME = "name";
private static final String SESSIONS = "sessions"; private static final String REDIRECT_URIS = "redirect_uris";
private static final String USERS = "users"; private static final String SECRET = "secret";
private static final String USER = "user"; private static final String SESSIONS = "sessions";
private static final String USERS = "users";
private static final String USER = "user";
private final Path storageFile; private final Path storageFile;
private final JSONObject json; private final JSONObject json;
@ -61,6 +63,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
@Override @Override
public FileStore init(User defaultUser) { public FileStore init(User defaultUser) {
if (!json.has(AUTHORIZATIONS)) json.put(AUTHORIZATIONS, new JSONObject());
if (!json.has(CLIENTS)) json.put(CLIENTS, new JSONObject()); if (!json.has(CLIENTS)) json.put(CLIENTS, new JSONObject());
if (!json.has(SESSIONS)) json.put(SESSIONS, new JSONObject()); if (!json.has(SESSIONS)) json.put(SESSIONS, new JSONObject());
if (!json.has(USERS)) save(defaultUser); if (!json.has(USERS)) save(defaultUser);
@ -179,6 +182,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
if (expiration.isAfter(Instant.now())) { if (expiration.isAfter(Instant.now())) {
return load(userId).map(user -> new Session(user, expiration, sessionId)); return load(userId).map(user -> new Session(user, expiration, sessionId));
} }
dropSession(sessionId);
} catch (Exception ignored) { } catch (Exception ignored) {
} }
return Optional.empty(); return Optional.empty();
@ -197,13 +201,6 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
/** client service methods **/ /** client service methods **/
@Override
public ClientService add(Client client) {
json.getJSONObject(CLIENTS).put(client.id(), Map.of(NAME, client.name(), SECRET, client.secret(), REDIRECT_URIS, client.redirectUris()));
save();
return this;
}
@Override @Override
public Optional<Client> getClient(String clientId) { public Optional<Client> getClient(String clientId) {
var clients = json.getJSONObject(CLIENTS); var clients = json.getJSONObject(CLIENTS);
@ -211,13 +208,6 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
return Optional.empty(); return Optional.empty();
} }
private Client toClient(String clientId, JSONObject clientData) {
var redirectUris = new HashSet<String>();
for (var o : clientData.getJSONArray(REDIRECT_URIS)) {
if (o instanceof String s) redirectUris.add(s);
}
return new Client(clientId, clientData.getString(NAME), clientData.getString(SECRET), redirectUris);
}
@Override @Override
public List<Client> listClients() { public List<Client> listClients() {
@ -235,19 +225,75 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
} }
@Override @Override
public ClientService update(Client client) { public ClientService save(Client client) {
return null; json.getJSONObject(CLIENTS).put(client.id(), Map.of(NAME, client.name(), SECRET, client.secret(), REDIRECT_URIS, client.redirectUris()));
save();
return this;
} }
private Client toClient(String clientId, JSONObject clientData) {
var redirectUris = new HashSet<String>();
for (var o : clientData.getJSONArray(REDIRECT_URIS)) {
if (o instanceof String s) redirectUris.add(s);
}
return new Client(clientId, clientData.getString(NAME), clientData.getString(SECRET), redirectUris);
}
/*** Authorization service methods ***/ /*** Authorization service methods ***/
@Override @Override
public AuthorizationService authorize(Client client, User user, Date expiration) { public Optional<Authorization> forCode(String code) {
return null; var authorizations = json.getJSONObject(AUTHORIZATIONS);
if (!authorizations.has(code)) return Optional.empty();
String authId = authorizations.getString(code);
if (!authorizations.has(authId)) {
authorizations.remove(code);
return Optional.empty();
}
try {
var expiration = Instant.ofEpochSecond(authorizations.getLong(authId));
if (expiration.isAfter(Instant.now())) {
String[] parts = authId.split("@");
return Optional.of(new Authorization(parts[1], parts[0], expiration));
}
authorizations.remove(authId);
} catch (Exception ignored) {
}
return Optional.empty();
}
@Override
public AuthorizationService addCode(Client client, User user, String code) {
var authorizations = json.getJSONObject(AUTHORIZATIONS);
authorizations.put(code, authorizationId(user, client));
save();
return this;
}
@Override
public AuthorizationService authorize(Client client, User user, Instant expiration) {
var authorizations = json.getJSONObject(AUTHORIZATIONS);
authorizations.put(authorizationId(user, client), expiration.getEpochSecond());
return this;
}
private String authorizationId(User user, Client client) {
return String.join("@", user.uuid(), client.id());
} }
@Override @Override
public boolean isAuthorized(Client client, User user) { public boolean isAuthorized(Client client, User user) {
var authorizations = json.getJSONObject(AUTHORIZATIONS);
var authId = authorizationId(user, client);
if (!authorizations.has(authId)) return false;
try {
if (Instant.ofEpochSecond(authorizations.getLong(authId)).isAfter(Instant.now())) return true;
} catch (Exception ignored) {
}
revoke(client, user);
return false; return false;
} }
@ -263,6 +309,10 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
@Override @Override
public AuthorizationService revoke(Client client, User user) { public AuthorizationService revoke(Client client, User user) {
return null; var authorizations = json.getJSONObject(AUTHORIZATIONS);
var authId = authorizationId(user, client);
if (!authorizations.has(authId)) return this;
authorizations.remove(authId);
return save();
} }
} }

5
de.srsoftware.oidc.web/src/main/resources/en/authorization.html

@ -12,7 +12,10 @@
<div id="content" style="display: none"> <div id="content" style="display: none">
<h1>Authorization</h1> <h1>Authorization</h1>
Confirmation required: are you shure you want to grant access to <span id="name">some client</span>? Confirmation required: are you shure you want to grant access to <span id="name">some client</span>?
<button type="button" onclick="grantAutorization()">Yes</button> <button type="button" onclick="grantAutorization(1)">Yes - 1 day</button>
<button type="button" onclick="grantAutorization(7)">Yes - 1 week</button>
<button type="button" onclick="grantAutorization(30)">Yes - 1 month</button>
<button type="button" onclick="grantAutorization(365)">Yes - 1 year</button>
<button type="button" onclick="denyAutorization()">No</button> <button type="button" onclick="denyAutorization()">No</button>
</div> </div>
<div id="error" class="error" style="display: none"></div> <div id="error" class="error" style="display: none"></div>

4
de.srsoftware.oidc.web/src/main/resources/en/authorization.js

@ -24,8 +24,8 @@ async function handleResponse(response){
} }
} }
function grantAutorization(){ function grantAutorization(days){
json.confirmed = true; json.days = days;
backendAutorization(); backendAutorization();
} }

10
de.srsoftware.oidc.web/src/main/resources/en/edit_client.html

@ -15,25 +15,25 @@
<table> <table>
<tr> <tr>
<th>ID</th> <th>ID</th>
<td><input type="text" disabled="true" id="client_id" /></td> <td><input type="text" disabled="true" id="client-id" /></td>
</tr> </tr>
<tr> <tr>
<th>Name</th> <th>Name</th>
<td><input type="text" id="name" /></td> <td><input type="text" id="client-name" /></td>
</tr> </tr>
<tr> <tr>
<th>Secret</th> <th>Secret</th>
<td><input type="text" id="secret" /></td> <td><input type="text" id="client-secret" /></td>
</tr> </tr>
<tr> <tr>
<th>Redirect URIs</th> <th>Redirect URIs</th>
<td> <td>
<textarea id="redirects"></textarea> <textarea id="redirect-urls"></textarea>
</td> </td>
</tr> </tr>
<tr> <tr>
<td></td> <td></td>
<td><button type="button" id="button">Update</button></td> <td><button type="button" id="button" onclick="updateClient();">Update</button></td>
</tr> </tr>
</table> </table>
</fieldset> </fieldset>

54
de.srsoftware.oidc.web/src/main/resources/en/edit_client.js

@ -1,20 +1,52 @@
var params = new URLSearchParams(window.location.search); var params = new URLSearchParams(window.location.search);
var id = params.get('id'); var id = params.get('id');
async function handleLoadResponse(response){
if (response.ok){
var json = await response.json();
get('client-id').value = json.client_id;
get('client-name').value = json.name;
get('client-secret').value = json.secret;
get('redirect-urls').value = json.redirect_uris.join("\n");
}
}
async function handleUpdateResponse(response){
if (response.ok) {
enable('button');
setText('button','saved.');
}
}
function resetButton(){
enable('button');
setText('button','Update')
}
function updateClient(){
disable('button');
setText('button','sent data…')
var data = {
client_id : getValue('client-id'),
name : getValue('client-name'),
secret : getValue('client-secret'),
redirect_uris : getValue('redirect-urls').split("\n")
};
fetch(client_controller+'/update',{
method : 'POST',
headers : {
'Content-Type': 'application/json'
},
body : JSON.stringify(data)
}).then(handleUpdateResponse);
setTimeout(resetButton,4000);
}
fetch(api+'/client', fetch(api+'/client',
{ {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
client_id : id client_id : id
}) })
}).then(handleResponse); }).then(handleLoadResponse);
async function handleResponse(response){
if (response.ok){
var json = await response.json();
get('client_id').value = json.client_id;
get('name').value = json.name;
get('secret').value = json.secret;
get('redirects').value = json.redirect_uris.join("\n");
}
}

2
de.srsoftware.oidc.web/src/main/resources/en/newclient.html → de.srsoftware.oidc.web/src/main/resources/en/new_client.html

@ -4,7 +4,7 @@
<title>Light OIDC</title> <title>Light OIDC</title>
<script src="common.js"></script> <script src="common.js"></script>
<script src="user.js"></script> <script src="user.js"></script>
<script src="newclient.js"></script> <script src="new_client.js"></script>
<link rel="stylesheet" href="style.css" /> <link rel="stylesheet" href="style.css" />
</head> </head>
<body> <body>

0
de.srsoftware.oidc.web/src/main/resources/en/newclient.js → de.srsoftware.oidc.web/src/main/resources/en/new_client.js

19
de.srsoftware.utils/build.gradle

@ -0,0 +1,19 @@
plugins {
id 'java'
}
group = 'de.srsoftware'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
}
test {
useJUnitPlatform()
}

13
de.srsoftware.utils/src/main/java/de/srsoftware/utils/Optionals.java

@ -0,0 +1,13 @@
/* © SRSoftware 2024 */
package de.srsoftware.utils;
import java.util.Optional;
public class Optionals {
public static <T> Optional<T> optional(T val) {
return Optional.ofNullable(val);
}
public static Optional<String> nonEmpty(String text) {
return text == null || text.isBlank() ? Optional.empty() : optional(text.trim());
}
}

19
de.srsoftware.utils/src/main/java/de/srsoftware/utils/Paths.java

@ -0,0 +1,19 @@
/* © SRSoftware 2024 */
package de.srsoftware.utils;
import java.nio.file.Path;
public class Paths {
public static Path configDir(String applicationName) {
String home = System.getProperty("user.home");
return Path.of(home, ".config", applicationName);
}
public static Path configDir(Class clazz) {
return configDir(clazz.getSimpleName());
}
public static Path configDir(Object clazz) {
return configDir(clazz.getClass());
}
}

1
settings.gradle

@ -6,4 +6,5 @@ include 'de.srsoftware.oidc.backend'
include 'de.srsoftware.oidc.datastore.file' include 'de.srsoftware.oidc.datastore.file'
include 'de.srsoftware.cookies' include 'de.srsoftware.cookies'
include 'de.srsoftware.logging' include 'de.srsoftware.logging'
include 'de.srsoftware.utils'

Loading…
Cancel
Save