001package co.codewizards.cloudstore.ls.rest.server.auth;
002
003import co.codewizards.cloudstore.core.io.ByteArrayInputStream;
004import java.io.CharArrayReader;
005import java.io.CharArrayWriter;
006import java.io.IOException;
007import java.io.InputStreamReader;
008import java.io.Reader;
009import java.io.UnsupportedEncodingException;
010import java.security.Principal;
011import java.util.Arrays;
012
013import javax.servlet.http.HttpServletRequest;
014import javax.ws.rs.NotAuthorizedException;
015import javax.ws.rs.WebApplicationException;
016import javax.ws.rs.container.ContainerRequestContext;
017import javax.ws.rs.container.ContainerRequestFilter;
018import javax.ws.rs.core.Context;
019import javax.ws.rs.core.MediaType;
020import javax.ws.rs.core.Response;
021import javax.ws.rs.core.Response.Status;
022import javax.ws.rs.core.SecurityContext;
023import javax.ws.rs.core.UriInfo;
024
025import org.glassfish.jersey.internal.util.Base64;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import co.codewizards.cloudstore.core.dto.Error;
030import co.codewizards.cloudstore.core.util.IOUtil;
031
032public class AuthFilter implements ContainerRequestFilter {
033
034        private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class);
035
036        protected @Context UriInfo uriInfo;
037
038        protected @Context HttpServletRequest request;
039
040        @Override
041        public void filter(ContainerRequestContext requestContext) throws IOException {
042                final String authorizationHeader = request.getHeader("Authorization");
043                if (authorizationHeader == null || authorizationHeader.isEmpty()) {
044                        logger.debug("getAuth: There is no 'Authorization' header. Replying with a Status.UNAUTHORIZED response asking for 'Basic' authentication.");
045                        throw newUnauthorizedException();
046                }
047
048                logger.debug("getAuth: 'Authorization' header: {}", authorizationHeader);
049
050                if (!authorizationHeader.startsWith("Basic"))
051                        throw new WebApplicationException(Response.status(Status.FORBIDDEN)
052                                        .type(MediaType.APPLICATION_XML)
053                                        .entity(new Error("Only 'Basic' authentication is supported!")).build());
054
055                final String basicAuthEncoded = authorizationHeader.substring("Basic".length()).trim();
056                final byte[] basicAuthDecodedBA = getBasicAuthEncodedBA(basicAuthEncoded);
057                final StringBuilder userNameSB = new StringBuilder();
058                char[] password = null;
059
060                final ByteArrayInputStream in = new ByteArrayInputStream(basicAuthDecodedBA);
061                char[] ca = null;
062                CharArrayWriter caw = new CharArrayWriter(basicAuthDecodedBA.length + 1);
063                CharArrayReader car = null;
064                try {
065                        final Reader r = new InputStreamReader(in, IOUtil.CHARSET_NAME_UTF_8);
066                        int charsReadTotal = 0;
067                        int charsRead;
068                        do {
069                                final char[] c = new char[10];
070                                charsRead = r.read(c);
071                                caw.write(c);
072
073                                if (charsRead > 0)
074                                        charsReadTotal += charsRead;
075                        } while (charsRead >= 0);
076
077                        charsRead = 0;
078
079                        car = new CharArrayReader(ca = caw.toCharArray());
080                        int charsReadTotalCheck = 0;
081
082                        while (charsRead >= 0 && charsRead < charsReadTotal) {
083                                final char[] cbuf = new char[1];
084                                charsRead = car.read(cbuf);
085                                if (charsRead > 0)
086                                        charsReadTotalCheck += charsRead;
087
088                                if (cbuf[0] == ':')
089                                        break;
090
091                                userNameSB.append(cbuf[0]);
092                        }
093
094                        if (charsRead >= 0 && charsRead < charsReadTotal) {
095                                password = new char[charsReadTotal - charsReadTotalCheck];
096                                final int passwordSize = car.read(password);
097                                if (passwordSize + charsReadTotalCheck != charsReadTotal)
098                                        throw new IllegalStateException("passwordSize and charsRead must match charsReadTotal!"
099                                                        + " passwordSize=" + passwordSize
100                                                        + ", charsRead=" + charsRead
101                                                        + ", charsReadTotal=" + charsReadTotal);//TODO for testing
102                        }
103                } catch (final Exception e) {
104                        throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).type(MediaType.APPLICATION_XML).entity(new Error(e)).build());
105                } finally {
106                        // For extra safety: Overwrite all sensitive memory with 0.
107                        Arrays.fill(basicAuthDecodedBA, (byte)0);
108                        
109                        if (ca != null)
110                                Arrays.fill(ca, (char)0);
111
112                        // overwrite caw:
113                        if (caw != null) {
114                                final char[] zeroArray = new char[caw.size()];
115                                caw.reset();
116                                try {
117                                        caw.write(zeroArray);
118                                        caw = null;
119                                } catch (final IOException e) {
120                                        throw new RuntimeException(e);
121                                }
122                        }
123                }
124
125                final String userName = userNameSB.toString(); // user-name is a unique client JVM identifier - not a real user name.
126                boolean passwordValid = AuthManager.getInstance().isPasswordValid(password);
127                Arrays.fill(password, (char)0); // password is not needed anymore => clear it
128                if (passwordValid) {
129                        requestContext.setSecurityContext(new SecurityContextImpl(userName, "https".equals(uriInfo.getRequestUri().getScheme())));
130                        return;
131                }
132                throw newUnauthorizedException();
133        }
134
135        public static class SecurityContextImpl implements SecurityContext {
136
137        private final Principal principal;
138        private final boolean secure;
139
140        public SecurityContextImpl(final String userName, final boolean secure) {
141                this.principal = new Principal() {
142                        @Override
143                                public String getName() {
144                                return userName;
145                        }
146                };
147                this.secure = secure;
148        }
149
150        @Override
151                public Principal getUserPrincipal() {
152            return principal;
153        }
154
155        /**
156         * @param role Role to be checked
157         */
158        @Override
159                public boolean isUserInRole(String role) {
160                if ("admin".equals(role)) {
161                        return false;
162                } else if ("user".equals(role)) {
163                        return principal != null;
164                }
165                return false;
166        }
167
168        @Override
169                public boolean isSecure() {
170                return secure;
171        }
172
173        @Override
174                public String getAuthenticationScheme() {
175                if (principal == null) {
176                        return null;
177                }
178                return SecurityContext.BASIC_AUTH;
179        }
180    }
181
182        private WebApplicationException newUnauthorizedException() {
183                return new NotAuthorizedException("Basic realm=\"CloudStoreServer.Local\"");
184        }
185
186        private byte[] getBasicAuthEncodedBA(final String basicAuthEncoded) {
187                byte[] basicAuthDecodedBA;
188                try {
189                        basicAuthDecodedBA = Base64.decode(basicAuthEncoded.getBytes(IOUtil.CHARSET_NAME_UTF_8));
190                } catch (final UnsupportedEncodingException e1) {
191                        throw new RuntimeException(e1);
192                }
193                return basicAuthDecodedBA;
194        }
195
196}