You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
240 lines
9.1 KiB
240 lines
9.1 KiB
/* © SRSoftware 2024 */ |
|
package de.srsoftware.oidc.backend; |
|
|
|
import static de.srsoftware.oidc.api.Constants.*; |
|
import static de.srsoftware.oidc.api.data.Permission.MANAGE_CLIENTS; |
|
import static de.srsoftware.tools.Error.error; |
|
import static java.net.HttpURLConnection.*; |
|
|
|
import com.sun.net.httpserver.HttpExchange; |
|
import de.srsoftware.oidc.api.*; |
|
import de.srsoftware.oidc.api.data.AuthorizedScopes; |
|
import de.srsoftware.oidc.api.data.Client; |
|
import de.srsoftware.oidc.api.data.Session; |
|
import de.srsoftware.oidc.api.data.User; |
|
import de.srsoftware.tools.Optionals; |
|
import java.io.IOException; |
|
import java.time.Duration; |
|
import java.time.Instant; |
|
import java.time.temporal.ChronoUnit; |
|
import java.util.*; |
|
import org.json.JSONObject; |
|
|
|
public class ClientController extends Controller { |
|
private static final System.Logger LOG = System.getLogger(ClientController.class.getSimpleName()); |
|
private final AuthorizationService authorizations; |
|
private final ClientService clients; |
|
private final UserService users; |
|
private final TokenController tokens; |
|
|
|
public ClientController(AuthorizationService authorizationService, ClientService clientService, SessionService sessionService, UserService userService, TokenController tokenController) { |
|
super(sessionService); |
|
authorizations = authorizationService; |
|
clients = clientService; |
|
users = userService; |
|
tokens = tokenController; |
|
} |
|
|
|
|
|
private boolean authorize(HttpExchange ex, Session session) throws IOException { |
|
var optUser = users.load(session.userId()); |
|
if (optUser.isEmpty()) return invalidSessionUser(ex); |
|
var user = optUser.get(); |
|
var json = json(ex); |
|
var state = json.has(STATE) ? json.getString(STATE) : null; |
|
if (!json.has(CLIENT_ID)) return badRequest(ex, error(ERROR_MISSING_PARAMETER).addData(PARAM, CLIENT_ID, STATE, state)); |
|
var clientId = json.getString(CLIENT_ID); |
|
var optClient = clients.getClient(clientId); |
|
if (optClient.isEmpty()) return badRequest(ex, error(ERROR_UNKNOWN_CLIENT).addData(CLIENT_ID, clientId, STATE, state)); |
|
for (String param : List.of(SCOPE, RESPONSE_TYPE, REDIRECT_URI)) { |
|
if (!json.has(param)) return badRequest(ex, error(ERROR_MISSING_PARAMETER).addData(PARAM, param, STATE, state)); |
|
} |
|
var scopes = toList(json, SCOPE); |
|
if (!scopes.contains(OPENID)) return badRequest(ex, error(ERROR_MISSING_PARAMETER).addData(PARAM, "Scope: openid", STATE, state)); |
|
var responseTypes = toList(json, RESPONSE_TYPE); |
|
var types = 0; |
|
for (var responseType : responseTypes) { |
|
switch (responseType) { |
|
case CODE: |
|
case ID_TOKEN: |
|
types++; |
|
break; |
|
default: |
|
return badRequest(ex, error(ERROR_UNSUPPORTED_RESPONSE_TYPE).addData(RESPONSE_TYPE, responseType, STATE, state)); |
|
} |
|
} |
|
if (types < 1) return badRequest(ex, error(ERROR_MISSING_CODE_RESPONSE_TYPE).addData(STATE, state)); |
|
|
|
var client = optClient.get(); |
|
var redirect = json.getString(REDIRECT_URI); |
|
|
|
if (!client.redirectUris().contains(redirect)) return badRequest(ex, error(ERROR_INVALID_REDIRECT).addData(REDIRECT_URI, redirect, STATE, state)); |
|
|
|
if (json.has(AUTHORZED)) { // user did consent |
|
var authorized = json.getJSONObject(AUTHORZED); |
|
var days = authorized.getInt("days"); |
|
var list = new ArrayList<String>(); |
|
authorized.getJSONArray("scopes").forEach(scope -> list.add(scope.toString())); |
|
authorizations.authorize(user.uuid(), client.id(), list, Instant.now().plus(days, ChronoUnit.DAYS)); |
|
} |
|
if (json.has(NONCE)) authorizations.nonce(user.uuid(), client.id(), json.getString(NONCE)); |
|
|
|
var authResult = authorizations.getAuthorization(user.uuid(), client.id(), scopes); |
|
if (!authResult.unauthorizedScopes().isEmpty()) { |
|
return sendContent(ex, Map.of("unauthorized_scopes", authResult.unauthorizedScopes(), "rp", client.name())); |
|
} |
|
|
|
var joinedAuthorizedScopes = Optionals.nullable(authResult.authorizedScopes()).map(AuthorizedScopes::scopes).map(list -> String.join(" ", list)); |
|
|
|
var result = new HashMap<String, Object>(); |
|
|
|
joinedAuthorizedScopes.ifPresent(authorizedScopes -> result.put(SCOPE, authorizedScopes)); |
|
|
|
if (responseTypes.contains(ID_TOKEN)) { |
|
var accessToken = users.accessToken(user); |
|
var issuer = hostname(ex); |
|
String jwToken = tokens.createJWT(client, user, accessToken, issuer); |
|
ex.getResponseHeaders().add("Cache-Control", "no-store"); |
|
result.put(ACCESS_TOKEN, accessToken.id()); |
|
result.put(TOKEN_TYPE, BEARER); |
|
result.put(EXPIRES_IN, 3600); |
|
result.put(ID_TOKEN, jwToken); |
|
} else if (responseTypes.contains(CODE)) { |
|
result.put(CODE, authResult.authCode()); |
|
if (state != null) result.put(STATE, state); |
|
} |
|
|
|
|
|
return sendContent(ex, result); |
|
} |
|
|
|
private List<String> toList(JSONObject json, String key) { |
|
return Arrays.asList(json.getString(key).split(" ")); |
|
} |
|
|
|
private boolean deleteClient(HttpExchange ex, Session session) throws IOException { |
|
var optUser = users.load(session.userId()); |
|
if (optUser.isEmpty()) return invalidSessionUser(ex); |
|
if (!optUser.get().hasPermission(MANAGE_CLIENTS)) return badRequest(ex, "NOT ALLOWED"); |
|
var json = json(ex); |
|
var id = json.getString(CLIENT_ID); |
|
clients.remove(id); |
|
return sendEmptyResponse(HTTP_OK, ex); |
|
} |
|
|
|
|
|
@Override |
|
public boolean doDelete(String path, HttpExchange ex) throws IOException { |
|
var optSession = getSession(ex); |
|
if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); |
|
|
|
// post-login paths |
|
var session = optSession.get(); |
|
var optUser = users.load(session.userId()); |
|
if (optUser.isEmpty()) return invalidSessionUser(ex); |
|
var user = optUser.get(); |
|
sessions.extend(session, user); |
|
|
|
switch (path) { |
|
case "/": |
|
return deleteClient(ex, session); |
|
} |
|
return notFound(ex); |
|
} |
|
|
|
@Override |
|
public boolean doGet(String path, HttpExchange ex) throws IOException { |
|
var optSession = getSession(ex); |
|
if (optSession.isEmpty()) return sendContent(ex, HTTP_UNAUTHORIZED, "No authorized!"); |
|
|
|
// post-login paths |
|
var session = optSession.get(); |
|
var optUser = users.load(session.userId()); |
|
if (optUser.isEmpty()) return invalidSessionUser(ex); |
|
var user = optUser.get(); |
|
sessions.extend(session, user); |
|
|
|
switch (path) { |
|
case "/dash": |
|
return dashboard(ex, user); |
|
case "/list": |
|
return list(ex, session); |
|
} |
|
return notFound(ex); |
|
} |
|
|
|
private boolean dashboard(HttpExchange ex, User user) throws IOException { |
|
var authorizedClients = authorizations // |
|
.authorizedClients(user.uuid()) |
|
.stream() |
|
.map(clients::getClient) |
|
.flatMap(Optional::stream) |
|
.sorted(Comparator.comparing(Client::name, String.CASE_INSENSITIVE_ORDER)) |
|
.map(Client::safeMap) |
|
.toList(); |
|
return sendContent(ex, Map.of(AUTHORZED, authorizedClients, NAME, user.realName())); |
|
} |
|
|
|
|
|
@Override |
|
public boolean doPost(String path, HttpExchange ex) throws IOException { |
|
var optSession = getSession(ex); |
|
if (optSession.isEmpty()) return sendContent(ex, HTTP_UNAUTHORIZED, "No authorized!"); |
|
|
|
// post-login paths |
|
var session = optSession.get(); |
|
var optUser = users.load(session.userId()); |
|
if (optUser.isEmpty()) return invalidSessionUser(ex); |
|
var user = optUser.get(); |
|
sessions.extend(session, user); |
|
|
|
switch (path) { |
|
case "/": |
|
return load(ex, session); |
|
case "/add", "/update": |
|
return save(ex, session); |
|
case "/authorize": |
|
return authorize(ex, session); |
|
} |
|
return notFound(ex); |
|
} |
|
|
|
private boolean list(HttpExchange ex, Session session) throws IOException { |
|
var optUser = users.load(session.userId()); |
|
if (optUser.isEmpty()) return invalidSessionUser(ex); |
|
if (!optUser.get().hasPermission(MANAGE_CLIENTS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); |
|
var json = new JSONObject(); |
|
clients.listClients().forEach(client -> json.put(client.id(), client.map())); |
|
return sendContent(ex, json); |
|
} |
|
|
|
|
|
private boolean load(HttpExchange ex, Session session) throws IOException { |
|
var optUser = users.load(session.userId()); |
|
if (optUser.isEmpty()) return invalidSessionUser(ex); |
|
if (!optUser.get().hasPermission(MANAGE_CLIENTS)) return sendEmptyResponse(HTTP_FORBIDDEN, ex); |
|
var json = json(ex); |
|
if (json.has(CLIENT_ID)) { |
|
var clientID = json.getString(CLIENT_ID); |
|
var client = clients.getClient(clientID).map(Client::map).map(JSONObject::new); |
|
if (client.isPresent()) return sendContent(ex, client.get()); |
|
} |
|
return sendEmptyResponse(HTTP_NOT_FOUND, ex); |
|
} |
|
|
|
private boolean save(HttpExchange ex, Session session) throws IOException { |
|
var optUser = users.load(session.userId()); |
|
if (optUser.isEmpty()) return invalidSessionUser(ex); |
|
if (!optUser.get().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 landingPage = json.has(LANDING_PAGE) ? json.getString(LANDING_PAGE) : null; |
|
var token_duration = Duration.ofMinutes(json.has(TOKEN_VALIDITY) ? json.getLong(TOKEN_VALIDITY) : 10); |
|
var client = new Client(json.getString(CLIENT_ID), json.getString(NAME), json.getString(SECRET), redirects).landingPage(landingPage).tokenValidity(token_duration); |
|
clients.save(client); |
|
return sendContent(ex, client); |
|
} |
|
}
|
|
|