de.hska.ld.oidc.controller.OIDCController.java Source code

Java tutorial

Introduction

Here is the source code for de.hska.ld.oidc.controller.OIDCController.java

Source

/*
 *  Code contributed to the Learning Layers project
 *  http://www.learning-layers.eu
 *  Development is partly funded by the FP7 Programme of the European
 *  Commission under Grant Agreement FP7-ICT-318209.
 *  Copyright (c) 2015, Karlsruhe University of Applied Sciences.
 *  For a list of contributors see the AUTHORS file at the top-level directory
 *  of this distribution.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package de.hska.ld.oidc.controller;

import de.hska.ld.content.events.document.DocumentEventsPublisher;
import de.hska.ld.content.persistence.domain.Attachment;
import de.hska.ld.content.persistence.domain.Document;
import de.hska.ld.content.service.DocumentService;
import de.hska.ld.core.exception.NotFoundException;
import de.hska.ld.core.exception.UserNotAuthorizedException;
import de.hska.ld.core.exception.ValidationException;
import de.hska.ld.core.persistence.domain.Role;
import de.hska.ld.core.persistence.domain.User;
import de.hska.ld.core.service.RoleService;
import de.hska.ld.core.service.UserService;
import de.hska.ld.core.util.Core;
import de.hska.ld.oidc.client.OIDCIdentityProviderClient;
import de.hska.ld.oidc.client.SSSClient;
import de.hska.ld.oidc.client.exception.AuthenticationNotValidException;
import de.hska.ld.oidc.dto.OIDCSubInfoDto;
import de.hska.ld.oidc.dto.OIDCUserinfoDto;
import de.hska.ld.oidc.dto.SSSAuthDto;
import de.hska.ld.oidc.dto.SSSLivingdocsResponseDto;
import de.hska.ld.oidc.persistence.domain.DocumentSSSInfo;
import de.hska.ld.oidc.service.DocumentSSSInfoService;
import de.hska.ld.oidc.service.UserSharingBufferService;
import org.mitre.openid.connect.client.SubjectIssuerGrantedAuthority;
import org.mitre.openid.connect.model.DefaultUserInfo;
import org.mitre.openid.connect.model.OIDCAuthenticationToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.exceptions.UnauthorizedClientException;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

@RestController
@RequestMapping(Core.RESOURCE_USER + "/oidc")
public class OIDCController {

    @Autowired
    private SSSClient sssClient;

    @Autowired
    private DocumentService documentService;

    @Autowired
    private UserService userService;

    @Autowired
    private RoleService roleService;

    @Autowired
    private Environment env;

    @Autowired
    private UserSharingBufferService userSharingBufferService;

    @Autowired
    private DocumentSSSInfoService documentSSSInfoService;

    @Autowired
    private DocumentEventsPublisher documentEventsPublisher;

    @RequestMapping(method = RequestMethod.GET, value = "/authenticate")
    public User authenticate(HttpServletRequest request,
            @RequestParam(defaultValue = "https://api.learning-layers.eu/o/oauth2") String issuer,
            @RequestHeader String Authorization, @RequestParam(defaultValue = "false") boolean forceUpdate) {
        try {
            return _authenticate(request, issuer, Authorization, forceUpdate);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ValidationException("login failed");
        }
    }

    private User _authenticate(HttpServletRequest request, String issuer, String Authorization) throws IOException {
        return this._authenticate(request, issuer, Authorization, false);
    }

    private User _authenticate(HttpServletRequest request, String issuer, String Authorization, boolean forceUpdate)
            throws IOException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // 1. check that the OIDC token is set in the correct way
        String oidcToken = null;
        try {
            oidcToken = Authorization.substring("Bearer ".length(), Authorization.length());
        } catch (Exception e) {
            throw new ValidationException("malformed oidc token information");
        }

        // 2. check if the oidcUserinfo is accessible via the access token
        OIDCUserinfoDto oidcUserinfoDto = authenticateTowardsOIDCIdentityProvider(issuer, oidcToken);
        if (!oidcUserinfoDto.isEmailVerified()) {
            throw new ValidationException("user email not verified");
        }

        // 3. check if a a user account for this oidc user still exists within living documents
        User user = userService.findBySubIdAndIssuer(oidcUserinfoDto.getSub(), issuer + "/");

        if (user == null) {
            // 3.1. If the user does NOT already exist:
            //      Create the new user in the database
            user = creatNewUserFromOIDCUserinfo(request, issuer, oidcUserinfoDto, oidcToken);
        } else {
            // 3.2. If the user does already exist:
            //      update the user's authentication
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            enrichAuthoritiesWithStoredAuthorities(request, oidcUserinfoDto.getSub(), issuer + "/", oidcUserinfoDto,
                    oidcToken, user, auth);
        }

        if (!authentication.isAuthenticated() || forceUpdate) {
            if (request.getSession(false) == null) {
                request.getSession(true);
            }
            // 4.   After the user data has been retrieved or created:
            //      Update the security context with the needed information (give the user access to other rest resources)
            Authentication auth = null;
            try {
                auth = SecurityContextHolder.getContext().getAuthentication();
            } catch (Exception e) {
                e.printStackTrace();
            }
            if (auth == null) {
                throw new ValidationException("no security context for this user available");
            } else {
                try {
                    Field detailsField = AbstractAuthenticationToken.class.getDeclaredField("details");
                    detailsField.setAccessible(true);
                    HttpSession session = request.getSession(false);
                    /*if (session == null || !session.isNew()) {
                       //session = request.getSession(true);
                    }*/
                    Field authenticatedField = AbstractAuthenticationToken.class.getDeclaredField("authenticated");
                    authenticatedField.setAccessible(true);
                    authenticatedField.set(auth, true);
                    SecurityContextHolder.getContext().setAuthentication(auth);
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }

        return user;
    }

    @RequestMapping(method = RequestMethod.GET, value = "/document/{documentId}/ispresent")
    @Transactional
    public ResponseEntity isDocumentPresent(HttpServletRequest request, @PathVariable Long documentId,
            @RequestParam(defaultValue = "https://api.learning-layers.eu/o/oauth2") String issuer,
            @RequestHeader(required = false) String Authorization) throws IOException, ServletException {

        _authenticate(request, issuer, Authorization);

        Document document = documentService.findById(documentId);
        if (document == null) {
            throw new NotFoundException("documentId");
        } else {
            return new ResponseEntity<>("exists", HttpStatus.OK);
        }
    }

    @RequestMapping(method = RequestMethod.POST, value = "/document/{documentId}/share/issuerandsub")
    @Transactional
    public ResponseEntity shareDocumentViaIssuerAndSub(HttpServletRequest request,
            @RequestBody List<OIDCSubInfoDto> issuerSubList, @PathVariable Long documentId,
            @RequestParam(defaultValue = "https://api.learning-layers.eu/o/oauth2") String issuer,
            @RequestHeader(required = false) String Authorization) throws IOException, ServletException {

        _authenticate(request, issuer, Authorization);

        Document document = documentService.findById(documentId);
        if (document == null) {
            throw new NotFoundException("documentId");
        }

        StringBuilder sb = new StringBuilder();
        boolean first = true;
        List<OIDCSubInfoDto> userIssuerAndSubsThatDontHaveALDAccountRightNow = new ArrayList<>();
        for (OIDCSubInfoDto isserSubDto : issuerSubList) {
            User user = userService.findBySubIdAndIssuer(isserSubDto.getSub(), isserSubDto.getIssuer());
            if (user != null && user.getId() != null && document.getCreator() != null
                    && document.getCreator().getId() != null
                    && !user.getId().equals(document.getCreator().getId())) {
                if (first) {
                    sb.append(user.getId());
                    first = false;
                } else {
                    sb.append(";").append(user.getId());
                }
            } else if (user == null) {
                // remember the issuer and subs used in the sharing process and store them in
                // case these users login into living documents at a later time
                userIssuerAndSubsThatDontHaveALDAccountRightNow.add(isserSubDto);
            }
        }

        try {
            // remember all the user sub and issuers that the document should be shared with but which are
            // not know to the system right now
            userIssuerAndSubsThatDontHaveALDAccountRightNow.forEach(isserSubDto -> {
                userSharingBufferService.addUserSharingBuffer(documentId, isserSubDto.getSub(),
                        isserSubDto.getIssuer(), "READ;WRITE");
            });
        } catch (Exception e) {
            e.printStackTrace();
        }

        String userIds = sb.toString();
        if (!"".equals(userIds)) {
            Document dbDocument = documentService.findById(document.getId());
            dbDocument = addAccessWithNewTransaction(dbDocument.getId(), userIds, "READ;WRITE");
            try {
                String oidcToken = null;
                try {
                    oidcToken = Authorization.substring("Bearer ".length(), Authorization.length());
                } catch (Exception e) {
                    throw new ValidationException("malformed oidc token information");
                }
                documentEventsPublisher.sendDocumentSharingEvent(dbDocument, oidcToken, dbDocument.getAccessList());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return new ResponseEntity<>(dbDocument.getAccessList(), HttpStatus.OK);
        } else {
            return new ResponseEntity<>(HttpStatus.OK);
        }
    }

    @RequestMapping(method = RequestMethod.POST, value = "/document/{documentId}/share/email")
    @Transactional
    public ResponseEntity shareDocumentViaEmailAdress(HttpServletRequest request,
            @RequestBody List<String> userEmailList, @PathVariable Long documentId,
            @RequestParam(defaultValue = "https://api.learning-layers.eu/o/oauth2") String issuer,
            @RequestHeader(required = false) String Authorization) throws IOException, ServletException {

        _authenticate(request, issuer, Authorization);

        Document document = documentService.findById(documentId);
        if (document == null) {
            throw new NotFoundException("documentId");
        }

        StringBuilder sb = new StringBuilder();
        boolean first = true;
        List<String> userEmailsThatDontHaveALDAccountRightNow = new ArrayList<>();
        for (String userEmail : userEmailList) {
            User user = userService.findByEmail(userEmail);
            if (user != null && user.getId() != null && document.getCreator() != null
                    && document.getCreator().getId() != null
                    && !user.getId().equals(document.getCreator().getId())) {
                if (first) {
                    sb.append(user.getId());
                    first = false;
                } else {
                    sb.append(";").append(user.getId());
                }
            } else if (user == null) {
                // remember the email address used in the sharing process and store them in
                // case these users login into living documents at a later time
                userEmailsThatDontHaveALDAccountRightNow.add(userEmail);
            }
        }

        // remember all the user emails that the document should be shared with but which are
        // not know to the system right now
        try {
            userEmailsThatDontHaveALDAccountRightNow.forEach(userEmail -> {
                userSharingBufferService.addUserSharingBuffer(documentId, userEmail, "READ;WRITE");
            });
        } catch (Exception e) {
            e.printStackTrace();
        }

        String userIds = sb.toString();
        Document dbDocument = null;
        if (!"".equals(userIds)) {
            dbDocument = documentService.findById(document.getId());
            dbDocument = addAccessWithNewTransaction(dbDocument.getId(), userIds, "READ;WRITE");
            try {
                String oidcToken = null;
                try {
                    oidcToken = Authorization.substring("Bearer ".length(), Authorization.length());
                } catch (Exception e) {
                    throw new ValidationException("malformed oidc token information");
                }
                documentEventsPublisher.sendDocumentSharingEvent(dbDocument, oidcToken, dbDocument.getAccessList());
            } catch (Exception e) {
                e.printStackTrace();
            }
            return new ResponseEntity<>(dbDocument.getAccessList(), HttpStatus.OK);
        } else {
            return new ResponseEntity<>(HttpStatus.OK);
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private Document addAccessWithNewTransaction(Long dbDocument, String userIds, String permissions) {
        return documentService.addAccessWithoutTransactional(dbDocument, userIds, permissions);
    }

    @RequestMapping(method = RequestMethod.POST, value = "/document")
    @Transactional(readOnly = false, rollbackFor = RuntimeException.class)
    public Document createDocument(HttpServletRequest request, @RequestBody Document document,
            @RequestParam(defaultValue = "https://api.learning-layers.eu/o/oauth2") String issuer,
            @RequestHeader(required = false) String Authorization,
            @RequestParam(required = false) String discussionId, @RequestParam(required = false) String episodeId)
            throws IOException, ServletException {

        _authenticate(request, issuer, Authorization);

        // 3. Create the document in the database
        Document newDocument = documentService.save(document);
        if (document.getDescription() != null) {
            Attachment mainAttachment = newDocument.getAttachmentList().get(0);
            mainAttachment.setSource(document.getDescription().getBytes());
            //document.setDescription("");
            documentService.save(newDocument);

            if (episodeId != null) {
                DocumentSSSInfo documentSSSInfo = new DocumentSSSInfo();
                documentSSSInfo.setDocument(newDocument);
                documentSSSInfo.setEpisodeId(episodeId);
                documentSSSInfoService.addDocumentInfo(documentSSSInfo);
            }
        }

        // 4. Create the document in the SSS together with the link to the discussion
        // 4.1 Authenticate with the SSS
        // SSS auth Endpoint: http://test-ll.know-center.tugraz.at/layers.test/auth/auth/
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        //if (auth instanceof AnonymousAuthenticationToken) {
        OIDCAuthenticationToken token = (OIDCAuthenticationToken) auth;
        SSSAuthDto sssAuthDto = null;
        try {
            sssAuthDto = sssClient.authenticate(token.getAccessTokenValue());
        } catch (UserNotAuthorizedException e) {
            request.logout();
            e.printStackTrace();
            throw new UnauthorizedClientException("oidc token invalid");
        }

        // 4.2 Create the according SSSLivingdocs entity
        try {
            SSSLivingdocsResponseDto sssLivingdocsResponseDto = sssClient.createDocument(document, discussionId,
                    token.getAccessTokenValue());
        } catch (AuthenticationNotValidException eAuth) {
            throw new UserNotAuthorizedException();
        }

        // 4.3 Retrieve the list of email addresses that have access to the livingdocument in the SSS
        // TODO retrieve email addresses

        return newDocument;
        /*} else {
        throw new UnauthorizedClientException("anonymous user session");
        }*/
    }

    private OIDCUserinfoDto authenticateTowardsOIDCIdentityProvider(String issuer, String oidcToken)
            throws ValidationException, IOException {
        // Retrieve oidc subject information from oidc identity provider
        // @ https://<oidc_endpoint>/userinfo?access_token=<accessToken>
        String[] allowedIssuers = new String[1];
        allowedIssuers[0] = "https://api.learning-layers.eu/o/oauth2";
        boolean issuerAllowed = false;
        for (String allowedIssuer : allowedIssuers) {
            if (allowedIssuer.equals(issuer)) {
                issuerAllowed = true;
            }
        }
        OIDCIdentityProviderClient client = new OIDCIdentityProviderClient();
        if (issuerAllowed) {
            return client.getUserinfo(issuer, oidcToken);
        } else {
            throw new ValidationException("issuer");
        }
    }

    @Transactional(readOnly = false)
    private User creatNewUserFromOIDCUserinfo(HttpServletRequest request, String issuer,
            OIDCUserinfoDto userInfoDto, String oidcToken) {
        // create a new user
        User user = new User();
        user.setEmail(userInfoDto.getEmail());
        // check for colliding user names (via preferred user name)
        User userWithGivenPreferredUserName = userService.findByUsername(userInfoDto.getPreferredUsername());
        int i = 0;
        if (userWithGivenPreferredUserName != null) {
            while (userWithGivenPreferredUserName != null) {
                String prefferedUsername = userInfoDto.getPreferredUsername() + "#" + i;
                userWithGivenPreferredUserName = userService.findByUsername(prefferedUsername);
            }
        } else {
            user.setUsername(userInfoDto.getPreferredUsername());
        }

        user.setFullName(userInfoDto.getName());
        user.setEnabled(true);
        // apply roles
        List<Role> roleList = new ArrayList<Role>();
        Role userRole = roleService.findByName("ROLE_USER");
        if (userRole == null) {
            // create initial roles
            String newUserRoleName = "ROLE_USER";
            userRole = createNewUserRole(newUserRoleName);
            String newAdminRoleName = "ROLE_ADMIN";
            Role adminRole = createNewUserRole(newAdminRoleName);
            // For the first user add the admin role
            roleList.add(adminRole);
        } else {
            roleList.add(userRole);
        }
        user.setRoleList(roleList);
        // A password is required so we set a uuid generated one
        if ("development".equals(env.getProperty("lds.app.instance"))) {
            user.setPassword("pass");
        } else {
            user.setPassword(UUID.randomUUID().toString());
        }
        user.setSubId(userInfoDto.getSub());
        user.setIssuer(issuer + "/");
        String oidcUpdatedTime = userInfoDto.getUpdatedTime();
        // oidc time: "20150701_090039"
        // oidc format: "yyyyMMdd_HHmmss"
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
        try {
            Date date = sdf.parse(oidcUpdatedTime);
            user.setLastupdatedAt(date);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        user = userService.save(user);
        // update security context
        // TODO set other attributes in SecurityContext
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        enrichAuthoritiesWithStoredAuthorities(request, userInfoDto.getSub(), issuer + "/", userInfoDto, oidcToken,
                user, auth);

        return user;
    }

    private Role createNewUserRole(String newRoleName) {
        Role newUserRole = new Role();
        newUserRole.setName(newRoleName);
        return roleService.save(newUserRole);
    }

    private void enrichAuthoritiesWithStoredAuthorities(HttpServletRequest request, String sub, String issuer,
            OIDCUserinfoDto oidcUserinfoDto, String oidcToken, User user, Authentication auth) {
        DefaultUserInfo userInfo = new DefaultUserInfo();
        userInfo.setSub(oidcUserinfoDto.getSub());
        userInfo.setEmail(oidcUserinfoDto.getEmail());
        userInfo.setName(oidcUserinfoDto.getName());
        userInfo.setEmailVerified(true);
        userInfo.setFamilyName(oidcUserinfoDto.getFamilyName());
        userInfo.setGivenName(oidcUserinfoDto.getGivenName());
        userInfo.setPreferredUsername(oidcUserinfoDto.getPreferredUsername());
        userInfo.setUpdatedTime(oidcUserinfoDto.getUpdatedTime());
        Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
        final SubjectIssuerGrantedAuthority[] oidcAuthority = new SubjectIssuerGrantedAuthority[1];
        authorities.forEach(authority -> {
            if (authority instanceof SubjectIssuerGrantedAuthority) {
                // extract the oidc authority information
                oidcAuthority[0] = (SubjectIssuerGrantedAuthority) authority;
            }
        });

        // create new authorities that includes the authorities stored in the database
        // as well as the oidc authority
        ArrayList<GrantedAuthority> newAuthorities = new ArrayList<GrantedAuthority>();
        user.getRoleList().forEach(role -> {
            newAuthorities.add(new SimpleGrantedAuthority(role.getName()));
        });
        if (oidcAuthority[0] == null) {
            newAuthorities.add(new SubjectIssuerGrantedAuthority(sub, issuer));
        } else {
            newAuthorities.add(oidcAuthority[0]);
        }
        OIDCAuthenticationToken token = new OIDCAuthenticationToken(sub, issuer, userInfo, newAuthorities, null,
                oidcToken, null);
        token.setDetails(new WebAuthenticationDetails(request));
        SecurityContextHolder.getContext().setAuthentication(token);
    }
}