View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.frontend.jersey;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.StringWriter;
24  import java.nio.file.Files;
25  import java.nio.file.StandardOpenOption;
26  import java.security.NoSuchAlgorithmException;
27  import java.security.SecureRandom;
28  import java.util.Date;
29  import java.util.Optional;
30  
31  import org.apache.logging.log4j.LogManager;
32  import org.mycore.common.MCRSession;
33  import org.mycore.common.MCRUserInformation;
34  import org.mycore.common.config.MCRConfiguration2;
35  import org.mycore.common.config.MCRConfigurationDir;
36  import org.mycore.common.config.MCRConfigurationException;
37  import org.mycore.common.events.MCRStartupHandler;
38  
39  import com.auth0.jwt.JWT;
40  import com.auth0.jwt.JWTCreator;
41  import com.auth0.jwt.algorithms.Algorithm;
42  import com.fasterxml.jackson.core.JsonFactory;
43  import com.fasterxml.jackson.core.JsonGenerator;
44  
45  import jakarta.servlet.ServletContext;
46  import jakarta.ws.rs.core.Response;
47  
48  public class MCRJWTUtil implements MCRStartupHandler.AutoExecutable {
49      public static final String JWT_CLAIM_ROLES = "mcr:roles";
50  
51      public static final String JWT_CLAIM_IP = "mcr:ip";
52  
53      public static final String JWT_USER_ATTRIBUTE_PREFIX = "mcr:ua:";
54  
55      public static final String JWT_SESSION_ATTRIBUTE_PREFIX = "mcr:sa:";
56  
57      private static final JsonFactory JSON_FACTORY = new JsonFactory();
58  
59      private static final String ROLES_PROPERTY = "MCR.Rest.JWT.Roles";
60  
61      private static Algorithm SHARED_SECRET;
62  
63      public static JWTCreator.Builder getJWTBuilder(MCRSession mcrSession) {
64          return getJWTBuilder(mcrSession, null, null);
65      }
66  
67      public static JWTCreator.Builder getJWTBuilder(MCRSession mcrSession,
68          String[] userAttributes, String[] sessionAttributes) {
69          MCRUserInformation userInformation = mcrSession.getUserInformation();
70          String[] roles = MCRConfiguration2.getOrThrow(ROLES_PROPERTY, MCRConfiguration2::splitValue)
71              .filter(userInformation::isUserInRole)
72              .toArray(String[]::new);
73          String subject = userInformation.getUserID();
74          String email = userInformation.getUserAttribute(MCRUserInformation.ATT_EMAIL);
75          String name = userInformation.getUserAttribute(MCRUserInformation.ATT_REAL_NAME);
76          JWTCreator.Builder builder = JWT.create()
77              .withIssuedAt(new Date())
78              .withSubject(subject)
79              .withArrayClaim("mcr:roles", roles)
80              .withClaim("email", email)
81              .withClaim("name", name);
82          if (userAttributes != null) {
83              for (String userAttribute : userAttributes) {
84                  String value = userInformation.getUserAttribute(userAttribute);
85                  if (value != null) {
86                      builder.withClaim(JWT_USER_ATTRIBUTE_PREFIX + userAttribute, value);
87                  }
88              }
89          }
90          if (sessionAttributes != null) {
91              for (String sessionAttribute : sessionAttributes) {
92                  Object object = mcrSession.get(sessionAttribute);
93                  Optional.ofNullable(object).map(Object::toString).ifPresent((value) -> {
94                      builder.withClaim(JWT_SESSION_ATTRIBUTE_PREFIX + sessionAttribute, value);
95                  });
96              }
97          }
98          return builder;
99      }
100 
101     public static Algorithm getJWTAlgorithm() {
102         return SHARED_SECRET;
103     }
104 
105     public static Response getJWTLoginSuccessResponse(String jwt) throws IOException {
106         try (StringWriter sw = new StringWriter()) {
107             JsonGenerator jsonGenerator = JSON_FACTORY.createGenerator(sw);
108             jsonGenerator.writeStartObject();
109             jsonGenerator.writeBooleanField("login_success", true);
110             jsonGenerator.writeStringField("access_token", jwt);
111             jsonGenerator.writeStringField("token_type", "Bearer");
112             jsonGenerator.writeEndObject();
113             jsonGenerator.flush();
114             jsonGenerator.close();
115             return Response.status(Response.Status.OK)
116                 .header("Authorization", "Bearer " + jwt)
117                 .entity(sw.toString())
118                 .build();
119         }
120     }
121 
122     public static Response getJWTRenewSuccessResponse(String jwt) throws IOException {
123         try (StringWriter sw = new StringWriter()) {
124             JsonGenerator jsonGenerator = JSON_FACTORY.createGenerator(sw);
125             jsonGenerator.writeStartObject();
126             jsonGenerator.writeBooleanField("executed", true);
127             jsonGenerator.writeStringField("access_token", jwt);
128             jsonGenerator.writeStringField("token_type", "Bearer");
129             jsonGenerator.writeEndObject();
130             jsonGenerator.flush();
131             jsonGenerator.close();
132             return Response.status(Response.Status.OK)
133                 .header("Authorization", "Bearer " + jwt)
134                 .entity(sw.toString())
135                 .build();
136         }
137     }
138 
139     public static Response getJWTLoginErrorResponse(String errorDescription) throws IOException {
140         try (StringWriter sw = new StringWriter()) {
141             JsonGenerator jsonGenerator = JSON_FACTORY.createGenerator(sw);
142             jsonGenerator.writeStartObject();
143             jsonGenerator.writeBooleanField("login_success", false);
144             jsonGenerator.writeStringField("error", "login_failed");
145             jsonGenerator.writeStringField("error_description", errorDescription);
146             jsonGenerator.writeEndObject();
147             jsonGenerator.flush();
148             jsonGenerator.close();
149             return Response.status(Response.Status.FORBIDDEN)
150                 .entity(sw.toString())
151                 .build();
152         }
153     }
154 
155     @Override
156     public String getName() {
157         return "JSON WebToken Services";
158     }
159 
160     @Override
161     public int getPriority() {
162         return 0;
163     }
164 
165     @Override
166     public void startUp(ServletContext servletContext) {
167         if (servletContext != null) {
168             File sharedSecretFile = MCRConfigurationDir.getConfigFile("jwt.secret");
169             byte[] secret;
170             if (!sharedSecretFile.isFile()) {
171                 secret = new byte[4096];
172                 try {
173                     LogManager.getLogger().warn(
174                         "Creating shared secret file ({}) for JSON Web Token."
175                             + " This may take a while. Please wait...",
176                         sharedSecretFile);
177                     SecureRandom.getInstanceStrong().nextBytes(secret);
178                     Files.write(sharedSecretFile.toPath(), secret, StandardOpenOption.CREATE_NEW);
179                 } catch (NoSuchAlgorithmException | IOException e) {
180                     throw new MCRConfigurationException(
181                         "Could not create shared secret in file: " + sharedSecretFile.getAbsolutePath(), e);
182                 }
183             } else {
184                 try {
185                     secret = Files.readAllBytes(sharedSecretFile.toPath());
186                 } catch (IOException e) {
187                     throw new MCRConfigurationException(
188                         "Could not create shared secret in file: " + sharedSecretFile.getAbsolutePath(), e);
189                 }
190             }
191             SHARED_SECRET = Algorithm.HMAC512(secret);
192         }
193     }
194 
195 }