Skip to content

Commit fbef28d

Browse files
Implement standalone launch sequence (#5)
1 parent e952f4c commit fbef28d

13 files changed

+440
-219
lines changed

omod/src/main/java/org/openmrs/module/smartonfhir/page/controller/FindPatientPageController.java

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,37 +9,33 @@
99
*/
1010
package org.openmrs.module.smartonfhir.page.controller;
1111

12+
import java.io.UnsupportedEncodingException;
13+
import java.net.URLEncoder;
14+
import java.nio.charset.StandardCharsets;
15+
1216
import org.openmrs.module.appframework.domain.AppDescriptor;
1317
import org.openmrs.module.appui.UiSessionContext;
1418
import org.openmrs.module.coreapps.helper.BreadcrumbHelper;
1519
import org.openmrs.ui.framework.UiUtils;
1620
import org.openmrs.ui.framework.page.PageModel;
1721
import org.springframework.web.bind.annotation.RequestParam;
1822

23+
@SuppressWarnings("unused")
1924
public class FindPatientPageController {
2025

21-
/**
22-
* This page is built to be shared across multiple apps. To use it, you must pass an "app" request
23-
* parameter, which must be the id of an existing app that is an instance of
24-
* coreapps.template.findPatient
25-
*
26-
* @param model
27-
* @param app
28-
* @param sessionContext
29-
*/
3026
public void get(PageModel model, @RequestParam("app") AppDescriptor app, @RequestParam("token") String token,
31-
UiSessionContext sessionContext, UiUtils ui) {
32-
System.out.println("in FindPatientPageController" + token);
33-
model.addAttribute("afterSelectedUrl", app.getConfig().get("afterSelectedUrl").getTextValue() + "&token=" + token);
27+
UiSessionContext sessionContext, UiUtils ui) throws UnsupportedEncodingException {
28+
model.addAttribute("afterSelectedUrl", app.getConfig().get("afterSelectedUrl").getTextValue() + "&token="
29+
+ URLEncoder.encode(token, StandardCharsets.UTF_8.name()));
3430
model.addAttribute("heading", app.getConfig().get("heading").getTextValue());
3531
model.addAttribute("label", app.getConfig().get("label").getTextValue());
3632
model.addAttribute("showLastViewedPatients", app.getConfig().get("showLastViewedPatients").getBooleanValue());
33+
3734
if (app.getConfig().get("registrationAppLink") == null) {
3835
model.addAttribute("registrationAppLink", "");
3936
} else {
4037
model.addAttribute("registrationAppLink", app.getConfig().get("registrationAppLink").getTextValue());
4138
}
4239
BreadcrumbHelper.addBreadcrumbsIfDefinedInApp(app, model, ui);
4340
}
44-
4541
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public License,
3+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
4+
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5+
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6+
*
7+
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8+
* graphic logo is a trademark of OpenMRS Inc.
9+
*/
10+
package org.openmrs.module.smartonfhir.web;
11+
12+
import com.fasterxml.jackson.annotation.JsonInclude;
13+
import com.fasterxml.jackson.annotation.JsonProperty;
14+
import lombok.Data;
15+
16+
@Data
17+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
18+
public class KeycloakConfig {
19+
20+
@JsonProperty(value = "realm", required = true)
21+
private String realm;
22+
23+
@JsonProperty(value = "auth-server-url", required = true)
24+
private String authServerUrl;
25+
26+
@JsonProperty(value = "ssl-required", required = false)
27+
private String sslRequired;
28+
29+
@JsonProperty(value = "resource", required = false)
30+
private String resource;
31+
32+
@JsonProperty(value = "public-client", required = false)
33+
private String publicClient;
34+
35+
@JsonProperty(value = "confidential-port", required = false)
36+
private int confidentialPort;
37+
}

omod/src/main/java/org/openmrs/module/smartonfhir/web/SmartConformance.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@
99
*/
1010
package org.openmrs.module.smartonfhir.web;
1111

12+
import com.fasterxml.jackson.annotation.JsonInclude;
1213
import com.fasterxml.jackson.annotation.JsonProperty;
1314
import lombok.Data;
1415

1516
@Data
17+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
1618
public class SmartConformance {
1719

18-
@JsonProperty("authorization_endpoint")
20+
@JsonProperty(value = "authorization_endpoint", required = true)
1921
private String authorizationEndpoint;
2022

21-
@JsonProperty("token_endpoint")
23+
@JsonProperty(value = "token_endpoint", required = true)
2224
private String tokenEndpoint;
2325

2426
@JsonProperty("token_endpoint_auth_methods_supported")
@@ -42,6 +44,6 @@ public class SmartConformance {
4244
@JsonProperty("revocation_endpoint")
4345
private String revocationEndpoint;
4446

45-
@JsonProperty("capabilities")
47+
@JsonProperty(value = "capabilities", required = true)
4648
private String[] capabilities;
4749
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public License,
3+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
4+
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5+
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6+
*
7+
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8+
* graphic logo is a trademark of OpenMRS Inc.
9+
*/
10+
package org.openmrs.module.smartonfhir.web;
11+
12+
import com.fasterxml.jackson.annotation.JsonInclude;
13+
import com.fasterxml.jackson.annotation.JsonProperty;
14+
import lombok.Data;
15+
16+
@Data
17+
@JsonInclude(JsonInclude.Include.NON_EMPTY)
18+
public class SmartSecretKey {
19+
20+
@JsonProperty(value = "smart-shared-secret-key", required = true)
21+
private String smartSharedSecretKey;
22+
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public License,
3+
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
4+
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5+
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6+
*
7+
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8+
* graphic logo is a trademark of OpenMRS Inc.
9+
*/
10+
package org.openmrs.module.smartonfhir.web.filter;
11+
12+
import javax.servlet.Filter;
13+
import javax.servlet.FilterChain;
14+
import javax.servlet.FilterConfig;
15+
import javax.servlet.ServletException;
16+
import javax.servlet.ServletRequest;
17+
import javax.servlet.ServletResponse;
18+
import javax.servlet.http.HttpServletRequest;
19+
import javax.servlet.http.HttpServletResponse;
20+
import javax.servlet.http.HttpSession;
21+
22+
import java.io.File;
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
import java.util.ArrayList;
26+
import java.util.Arrays;
27+
import java.util.List;
28+
import java.util.regex.Matcher;
29+
import java.util.regex.Pattern;
30+
import java.util.stream.Collectors;
31+
32+
import com.fasterxml.jackson.databind.ObjectMapper;
33+
import lombok.extern.slf4j.Slf4j;
34+
import org.apache.commons.lang.StringUtils;
35+
import org.keycloak.common.util.Base64;
36+
import org.keycloak.jose.jws.JWSInput;
37+
import org.keycloak.jose.jws.JWSInputException;
38+
import org.keycloak.jose.jws.crypto.HMACProvider;
39+
import org.keycloak.representations.JsonWebToken;
40+
import org.openmrs.api.context.Context;
41+
import org.openmrs.api.context.ContextAuthenticationException;
42+
import org.openmrs.module.smartonfhir.web.SmartSecretKey;
43+
import org.openmrs.module.smartonfhir.web.smart.SmartTokenCredentials;
44+
import org.openmrs.util.OpenmrsUtil;
45+
import org.springframework.core.io.Resource;
46+
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
47+
48+
@Slf4j
49+
public class AuthenticationByPassFilter implements Filter {
50+
51+
public static final String SMART_AUTH_BYPASS = "SMART_AUTH_BYPASS";
52+
53+
private static final String VALID_URLS_PARAM = "validUrls";
54+
55+
private static final Pattern KEY_PARAM = Pattern.compile("^key=([^&]*)(?:&|$)");
56+
57+
private static final ObjectMapper objectMapper = new ObjectMapper();
58+
59+
private SmartSecretKey smartSecretKey;
60+
61+
private List<String> validUrls = new ArrayList<>(0);
62+
63+
@Override
64+
public void init(FilterConfig filterConfig) {
65+
String validUrlsParam = filterConfig.getInitParameter(VALID_URLS_PARAM);
66+
if (StringUtils.isNotBlank(validUrlsParam)) {
67+
validUrls = Arrays.stream(validUrlsParam.split(",")).filter(org.apache.commons.lang3.StringUtils::isNotBlank)
68+
.map(it -> {
69+
if (it.startsWith("/") || it.equals("*")) {
70+
return it;
71+
}
72+
73+
return "/" + it;
74+
}).distinct().collect(Collectors.toList());
75+
}
76+
}
77+
78+
@Override
79+
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
80+
throws IOException, ServletException {
81+
HttpServletRequest request = (HttpServletRequest) servletRequest;
82+
HttpServletResponse response = (HttpServletResponse) servletResponse;
83+
84+
if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
85+
Context.logout();
86+
}
87+
88+
String pathInfo = request.getRequestURI();
89+
90+
boolean isValidRequest = false;
91+
if (pathInfo != null) {
92+
final String thePathInfo = pathInfo.replaceFirst(request.getContextPath(), "");
93+
isValidRequest = validUrls.stream().anyMatch(url -> {
94+
if (url.endsWith("*")) {
95+
if (url.length() < 3) {
96+
return true;
97+
} else {
98+
String urlStart = url.substring(0, url.length() - 2);
99+
return thePathInfo.startsWith(urlStart);
100+
}
101+
}
102+
103+
return url.equals(thePathInfo);
104+
});
105+
}
106+
107+
if (!isValidRequest) {
108+
HttpSession session = request.getSession(false);
109+
if (session != null && session.getAttribute(SMART_AUTH_BYPASS) != null) {
110+
session.invalidate();
111+
Context.logout();
112+
request.getSession();
113+
}
114+
115+
filterChain.doFilter(request, response);
116+
return;
117+
}
118+
119+
if (!Context.isAuthenticated()) {
120+
final String tokenParam = request.getParameter("token");
121+
122+
if (tokenParam != null) {
123+
int keyPos = tokenParam.indexOf("key=");
124+
if (keyPos >= 0) {
125+
Matcher m = KEY_PARAM.matcher(tokenParam.substring(keyPos));
126+
if (m.find()) {
127+
final String key = m.group(1);
128+
129+
final String userToken;
130+
try {
131+
JWSInput jwsInput = new JWSInput(key);
132+
JsonWebToken webToken = jwsInput.readJsonContent(JsonWebToken.class);
133+
134+
if (!webToken.isActive()) {
135+
log.error("Token has expired");
136+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not authenticated");
137+
return;
138+
}
139+
140+
userToken = (String) webToken.getOtherClaims().get("user");
141+
}
142+
catch (JWSInputException e) {
143+
log.error("Error while reading JWS token", e);
144+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not authenticated");
145+
return;
146+
}
147+
148+
if (userToken == null) {
149+
log.error("Could not read user entry from token");
150+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not authenticated");
151+
return;
152+
}
153+
154+
final String username;
155+
try {
156+
JWSInput jwsInput = new JWSInput(userToken);
157+
if (!HMACProvider.verify(jwsInput, Base64.decode(getSecretKey()))) {
158+
log.error("Error validating user token");
159+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not authenticated");
160+
return;
161+
}
162+
163+
JsonWebToken webToken = jwsInput.readJsonContent(JsonWebToken.class);
164+
username = webToken.getSubject();
165+
}
166+
catch (JWSInputException e) {
167+
log.error("Error while reading user token", e);
168+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not authenticated");
169+
return;
170+
}
171+
172+
try {
173+
Context.authenticate(new SmartTokenCredentials(username));
174+
Context.getUserContext().setLocation(Context.getLocationService().getDefaultLocation());
175+
request.getSession().setAttribute(SMART_AUTH_BYPASS, true);
176+
}
177+
catch (ContextAuthenticationException e) {
178+
log.error("Error while logging in as user {}", username, e);
179+
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not authenticated");
180+
return;
181+
}
182+
}
183+
}
184+
}
185+
}
186+
187+
filterChain.doFilter(request, response);
188+
}
189+
190+
private String getSecretKey() throws IOException {
191+
final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
192+
Resource resource = resolver.getResource(
193+
OpenmrsUtil.getDirectoryInApplicationDataDirectory("config") + File.separator + "smart-secret-key.json");
194+
if (resource != null) {
195+
resource = resolver.getResource("classpath:smart-secret-key.json");
196+
197+
InputStream secretKeyStream = resource.getInputStream();
198+
199+
smartSecretKey = new SmartSecretKey();
200+
smartSecretKey = objectMapper.readValue(secretKeyStream, SmartSecretKey.class);
201+
}
202+
203+
return smartSecretKey.getSmartSharedSecretKey();
204+
}
205+
206+
@Override
207+
public void destroy() {
208+
}
209+
}

0 commit comments

Comments
 (0)