no message

This commit is contained in:
Ioannis Kalyvas 2018-02-23 12:36:51 +02:00
parent 70db1b2894
commit 4948737142
33 changed files with 444 additions and 42 deletions

View File

@ -28,6 +28,8 @@ public class CredentialBuilder extends Builder<Credential> {
private Date lastUpdateTime; private Date lastUpdateTime;
private String externalId;
public CredentialBuilder id(UUID id) { public CredentialBuilder id(UUID id) {
this.id = id; this.id = id;
return this; return this;
@ -68,6 +70,11 @@ public class CredentialBuilder extends Builder<Credential> {
return this; return this;
} }
public CredentialBuilder externalId(String externalId) {
this.externalId = externalId;
return this;
}
public Credential build() { public Credential build() {
Credential credential = new Credential(); Credential credential = new Credential();
credential.setStatus(status); credential.setStatus(status);
@ -78,6 +85,7 @@ public class CredentialBuilder extends Builder<Credential> {
credential.setPublicValue(publicValue); credential.setPublicValue(publicValue);
credential.setUserInfo(userInfo); credential.setUserInfo(userInfo);
credential.setId(id); credential.setId(id);
credential.setExternalId(externalId);
return credential; return credential;
} }
} }

View File

@ -7,6 +7,9 @@ import eu.eudat.models.login.Credentials;
import eu.eudat.models.login.LoginInfo; import eu.eudat.models.login.LoginInfo;
import eu.eudat.models.security.Principal; import eu.eudat.models.security.Principal;
import eu.eudat.security.CustomAuthenticationProvider; import eu.eudat.security.CustomAuthenticationProvider;
import eu.eudat.security.validators.b2access.B2AccessTokenValidator;
import eu.eudat.security.validators.b2access.helpers.B2AccessRequest;
import eu.eudat.security.validators.b2access.helpers.B2AccessResponseToken;
import eu.eudat.security.validators.twitter.TwitterTokenValidator; import eu.eudat.security.validators.twitter.TwitterTokenValidator;
import eu.eudat.services.AuthenticationService; import eu.eudat.services.AuthenticationService;
import eu.eudat.types.ApiMessageCode; import eu.eudat.types.ApiMessageCode;
@ -30,11 +33,14 @@ public class Login {
private TwitterTokenValidator twitterTokenValidator; private TwitterTokenValidator twitterTokenValidator;
private B2AccessTokenValidator b2AccessTokenValidator;
@Autowired @Autowired
public Login(CustomAuthenticationProvider customAuthenticationProvider, AuthenticationService authenticationService, TwitterTokenValidator twitterTokenValidator) { public Login(CustomAuthenticationProvider customAuthenticationProvider, AuthenticationService authenticationService, TwitterTokenValidator twitterTokenValidator, B2AccessTokenValidator b2AccessTokenValidator) {
this.customAuthenticationProvider = customAuthenticationProvider; this.customAuthenticationProvider = customAuthenticationProvider;
this.authenticationService = authenticationService; this.authenticationService = authenticationService;
this.twitterTokenValidator = twitterTokenValidator; this.twitterTokenValidator = twitterTokenValidator;
this.b2AccessTokenValidator = b2AccessTokenValidator;
} }
@Transactional @Transactional
@ -76,6 +82,17 @@ public class Login {
} }
} }
@RequestMapping(method = RequestMethod.POST, value = {"/b2AccessRequestToken"}, produces = "application/json", consumes = "application/json")
public @ResponseBody
ResponseEntity<ResponseItem<B2AccessResponseToken>> b2AccessRequestToken(@RequestBody B2AccessRequest b2AccessRequest) {
try {
return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem<B2AccessResponseToken>().payload(this.b2AccessTokenValidator.getAccessToken(b2AccessRequest)).status(ApiMessageCode.NO_MESSAGE));
} catch (Exception ex) {
ex.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ResponseItem<B2AccessResponseToken>().status(ApiMessageCode.DEFAULT_ERROR_MESSAGE).message(ex.getMessage()));
}
}
@RequestMapping(method = RequestMethod.POST, value = {"/me"}, consumes = "application/json", produces = "application/json") @RequestMapping(method = RequestMethod.POST, value = {"/me"}, consumes = "application/json", produces = "application/json")
public @ResponseBody public @ResponseBody
ResponseEntity<ResponseItem<Principal>> authMe(Principal principal) { ResponseEntity<ResponseItem<Principal>> authMe(Principal principal) {

View File

@ -7,6 +7,11 @@ import java.util.UUID;
@Entity @Entity
@Table(name = "\"Credential\"") @Table(name = "\"Credential\"")
@NamedEntityGraphs({
@NamedEntityGraph(
name = "credentialUserInfo",
attributeNodes = {@NamedAttributeNode("userInfo")})
})
public class Credential implements DataEntity<Credential,UUID> { public class Credential implements DataEntity<Credential,UUID> {
@Id @Id
@ -31,6 +36,9 @@ public class Credential implements DataEntity<Credential,UUID> {
@Column(name = "\"LastUpdateTime\"", nullable = false) @Column(name = "\"LastUpdateTime\"", nullable = false)
private Date lastUpdateTime; private Date lastUpdateTime;
@Column(name = "\"ExternalId\"", nullable = false)
private String externalId;
public UUID getId() { public UUID getId() {
return id; return id;
} }
@ -95,6 +103,14 @@ public class Credential implements DataEntity<Credential,UUID> {
this.lastUpdateTime = lastUpdateTime; this.lastUpdateTime = lastUpdateTime;
} }
public String getExternalId() {
return externalId;
}
public void setExternalId(String externalId) {
this.externalId = externalId;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -6,6 +6,7 @@ import eu.eudat.models.helpers.common.Ordering;
import eu.eudat.models.helpers.requests.TableRequest; import eu.eudat.models.helpers.requests.TableRequest;
import eu.eudat.queryable.QueryableList; import eu.eudat.queryable.QueryableList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
public class PaginationManager { public class PaginationManager {
@ -14,6 +15,8 @@ public class PaginationManager {
if (tableRequest.getOrderings() != null) applyOrder(items, tableRequest); if (tableRequest.getOrderings() != null) applyOrder(items, tableRequest);
if (tableRequest.getLength() != null) items.take(tableRequest.getLength()); if (tableRequest.getLength() != null) items.take(tableRequest.getLength());
if (tableRequest.getOffset() != null) items.skip(tableRequest.getOffset()); if (tableRequest.getOffset() != null) items.skip(tableRequest.getOffset());
if (tableRequest.getSelection() != null && tableRequest.getSelection().getFields() != null && tableRequest.getSelection().getFields().length > 0)
items.withFields(Arrays.asList(tableRequest.getSelection().getFields()));
return items; return items;
} }

View File

@ -5,11 +5,20 @@ import eu.eudat.security.validators.TokenValidatorFactoryImpl;
public class LoginProviderUser { public class LoginProviderUser {
private String name; private String name;
private String id;
private String email; private String email;
private String secret; private String secret;
private boolean isVerified; private boolean isVerified;
private TokenValidatorFactoryImpl.LoginProvider provider; private TokenValidatorFactoryImpl.LoginProvider provider;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() { public String getName() {
return name; return name;
} }

View File

@ -22,6 +22,8 @@ public interface QueryableList<T extends DataEntity> {
List<T> toList(); List<T> toList();
QueryableList<T> withFields(List<String> fields);
CompletableFuture<List<T>> toListAsync(); CompletableFuture<List<T>> toListAsync();
T getSingle(); T getSingle();

View File

@ -33,7 +33,7 @@ public class QueryableHibernateList<T extends DataEntity> implements QueryableLi
private List<NestedQuerySinglePredicate<T>> nestedPredicates = new LinkedList<>(); private List<NestedQuerySinglePredicate<T>> nestedPredicates = new LinkedList<>();
private List<OrderByPredicate<T>> orderings = new LinkedList<>(); private List<OrderByPredicate<T>> orderings = new LinkedList<>();
private List<Selection> fields = new LinkedList<>(); private List<String> fields = new LinkedList<>();
private Integer length; private Integer length;
private Integer offset; private Integer offset;
private Set<String> hints; private Set<String> hints;
@ -59,6 +59,18 @@ public class QueryableHibernateList<T extends DataEntity> implements QueryableLi
return this; return this;
} }
@Override
public QueryableList<T> withFields(List<String> fields) {
this.fields = fields;
return this;
}
private QueryableList<T> selectFields() {
List<Selection> rootFields = fields.stream().map(field -> root.get(field)).collect(Collectors.toList());
this.query.select(this.manager.getCriteriaBuilder().construct(tClass, rootFields.toArray(new Selection[rootFields.size()])));
return this;
}
public QueryableHibernateList<T> setEntity(Class<T> type) { public QueryableHibernateList<T> setEntity(Class<T> type) {
CriteriaBuilder builder = this.manager.getCriteriaBuilder(); CriteriaBuilder builder = this.manager.getCriteriaBuilder();
this.query = builder.createQuery(type); this.query = builder.createQuery(type);
@ -98,7 +110,7 @@ public class QueryableHibernateList<T extends DataEntity> implements QueryableLi
} }
public <R> CompletableFuture<List<R>> selectAsync(SelectPredicate<T, R> predicate) { public <R> CompletableFuture<List<R>> selectAsync(SelectPredicate<T, R> predicate) {
return this.toListAsync().thenApplyAsync(items-> items.stream().map(item -> predicate.applySelection(item)).collect(Collectors.toList())); return this.toListAsync().thenApplyAsync(items -> items.stream().map(item -> predicate.applySelection(item)).collect(Collectors.toList()));
} }
public QueryableList<T> distinct() { public QueryableList<T> distinct() {
@ -162,9 +174,9 @@ public class QueryableHibernateList<T extends DataEntity> implements QueryableLi
} }
public List<T> toList() { public List<T> toList() {
this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot)); this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot));
if (!this.orderings.isEmpty()) this.query.orderBy(this.generateOrderPredicates(this.orderings, this.root)); if (!this.orderings.isEmpty()) this.query.orderBy(this.generateOrderPredicates(this.orderings, this.root));
if (this.fields != null && !this.fields.isEmpty()) this.selectFields();
TypedQuery<T> typedQuery = this.manager.createQuery(this.query); TypedQuery<T> typedQuery = this.manager.createQuery(this.query);
if (this.offset != null) typedQuery.setFirstResult(this.offset); if (this.offset != null) typedQuery.setFirstResult(this.offset);
if (this.length != null) typedQuery.setMaxResults(this.length); if (this.length != null) typedQuery.setMaxResults(this.length);
@ -178,6 +190,7 @@ public class QueryableHibernateList<T extends DataEntity> implements QueryableLi
public CompletableFuture<List<T>> toListAsync() { public CompletableFuture<List<T>> toListAsync() {
this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot)); this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot));
if (!this.orderings.isEmpty()) this.query.orderBy(this.generateOrderPredicates(this.orderings, this.root)); if (!this.orderings.isEmpty()) this.query.orderBy(this.generateOrderPredicates(this.orderings, this.root));
if (this.fields != null && !this.fields.isEmpty()) this.selectFields();
TypedQuery<T> typedQuery = this.manager.createQuery(this.query); TypedQuery<T> typedQuery = this.manager.createQuery(this.query);
if (this.offset != null) typedQuery.setFirstResult(this.offset); if (this.offset != null) typedQuery.setFirstResult(this.offset);
if (this.length != null) typedQuery.setMaxResults(this.length); if (this.length != null) typedQuery.setMaxResults(this.length);
@ -192,6 +205,7 @@ public class QueryableHibernateList<T extends DataEntity> implements QueryableLi
public T getSingle() { public T getSingle() {
this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot)); this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot));
if (this.fields != null && !this.fields.isEmpty()) this.selectFields();
TypedQuery<T> typedQuery = this.manager.createQuery(this.query); TypedQuery<T> typedQuery = this.manager.createQuery(this.query);
if (this.hint != null) if (this.hint != null)
typedQuery.setHint("javax.persistence.fetchgraph", this.manager.getEntityGraph(this.hint)); typedQuery.setHint("javax.persistence.fetchgraph", this.manager.getEntityGraph(this.hint));
@ -200,6 +214,7 @@ public class QueryableHibernateList<T extends DataEntity> implements QueryableLi
public CompletableFuture<T> getSingleAsync() { public CompletableFuture<T> getSingleAsync() {
this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot)); this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot));
if (this.fields != null && !this.fields.isEmpty()) this.selectFields();
TypedQuery<T> typedQuery = this.manager.createQuery(this.query); TypedQuery<T> typedQuery = this.manager.createQuery(this.query);
if (this.hint != null) if (this.hint != null)
typedQuery.setHint("javax.persistence.fetchgraph", this.manager.getEntityGraph(this.hint)); typedQuery.setHint("javax.persistence.fetchgraph", this.manager.getEntityGraph(this.hint));
@ -208,7 +223,7 @@ public class QueryableHibernateList<T extends DataEntity> implements QueryableLi
public T getSingleOrDefault() { public T getSingleOrDefault() {
this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot)); this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot));
if (this.fields != null && !this.fields.isEmpty()) this.selectFields();
TypedQuery<T> typedQuery = this.manager.createQuery(this.query); TypedQuery<T> typedQuery = this.manager.createQuery(this.query);
if (this.hint != null) if (this.hint != null)
typedQuery.setHint("javax.persistence.fetchgraph", this.manager.getEntityGraph(this.hint)); typedQuery.setHint("javax.persistence.fetchgraph", this.manager.getEntityGraph(this.hint));
@ -220,7 +235,7 @@ public class QueryableHibernateList<T extends DataEntity> implements QueryableLi
public CompletableFuture<T> getSingleOrDefaultAsync() { public CompletableFuture<T> getSingleOrDefaultAsync() {
this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot)); this.query.where(this.generateWherePredicates(this.singlePredicates, this.root, this.nestedPredicates, this.nestedQueryRoot));
if (this.fields != null && !this.fields.isEmpty()) this.selectFields();
TypedQuery<T> typedQuery = this.manager.createQuery(this.query); TypedQuery<T> typedQuery = this.manager.createQuery(this.query);
if (this.hint != null) if (this.hint != null)
typedQuery.setHint("javax.persistence.fetchgraph", this.manager.getEntityGraph(this.hint)); typedQuery.setHint("javax.persistence.fetchgraph", this.manager.getEntityGraph(this.hint));

View File

@ -0,0 +1,12 @@
package eu.eudat.security.customproviders;
import eu.eudat.security.validators.b2access.helpers.B2AccessResponseToken;
/**
* Created by ikalyvas on 2/22/2018.
*/
public interface B2AccessCustomProvider {
B2AccessUser getUser(String accessToken);
B2AccessResponseToken getAccessToken(String code, String redirectUri, String clientId, String clientSecret);
}

View File

@ -0,0 +1,80 @@
package eu.eudat.security.customproviders;
import com.google.api.client.repackaged.org.apache.commons.codec.binary.Base64;
import eu.eudat.security.validators.b2access.helpers.B2AccessResponseToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
import java.util.Map;
/**
* Created by ikalyvas on 2/22/2018.
*/
@Component("b2AccessCustomProvider")
public class B2AccessCustomProviderImpl implements B2AccessCustomProvider {
private Environment environment;
@Autowired
public B2AccessCustomProviderImpl(Environment environment) {
this.environment = environment;
}
public B2AccessUser getUser(String accessToken) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = this.createBearerAuthHeaders(accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
Map<String, Object> values = restTemplate.exchange(this.environment.getProperty("b2access.externallogin.user_info_url"), HttpMethod.GET, entity, Map.class).getBody();
B2AccessUser b2AccessUser = new B2AccessUser();
b2AccessUser.setEmail((String)values.get("email"));
b2AccessUser.setId((String)values.get("urn:oid:2.5.4.49"));
b2AccessUser.setName((String)values.get("name"));
return b2AccessUser;
}
@Override
public B2AccessResponseToken getAccessToken(String code, String redirectUri, String clientId, String clientSecret) {
RestTemplate template = new RestTemplate();
HttpHeaders headers = this.createBasicAuthHeaders(clientId, clientSecret);
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
map.add("code", code);
map.add("grant_type", "authorization_code");
map.add("redirect_uri", redirectUri);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);
Map<String, Object> values = template.postForObject(this.environment.getProperty("b2access.externallogin.access_token_url"), request, Map.class);
B2AccessResponseToken b2AccessResponseToken = new B2AccessResponseToken();
b2AccessResponseToken.setAccessToken((String) values.get("access_token"));
return b2AccessResponseToken;
}
private HttpHeaders createBasicAuthHeaders(String username, String password) {
return new HttpHeaders() {{
String auth = username + ":" + password;
byte[] encodedAuth = Base64.encodeBase64(
auth.getBytes(Charset.forName("US-ASCII")));
String authHeader = "Basic " + new String(encodedAuth);
set("Authorization", authHeader);
}};
}
private HttpHeaders createBearerAuthHeaders(String accessToken) {
return new HttpHeaders() {{
String authHeader = "Bearer " + new String(accessToken);
set("Authorization", authHeader);
}};
}
}

View File

@ -0,0 +1,34 @@
package eu.eudat.security.customproviders;
/**
* Created by ikalyvas on 2/22/2018.
*/
public class B2AccessUser {
private String id;
private String name;
private String email;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}

View File

@ -1,5 +1,6 @@
package eu.eudat.security.validators; package eu.eudat.security.validators;
import eu.eudat.security.validators.b2access.B2AccessTokenValidator;
import eu.eudat.security.validators.facebook.FacebookTokenValidator; import eu.eudat.security.validators.facebook.FacebookTokenValidator;
import eu.eudat.security.validators.google.GoogleTokenValidator; import eu.eudat.security.validators.google.GoogleTokenValidator;
import eu.eudat.security.validators.linkedin.LinkedInTokenValidator; import eu.eudat.security.validators.linkedin.LinkedInTokenValidator;
@ -11,7 +12,7 @@ import org.springframework.stereotype.Service;
@Service("tokenValidatorFactory") @Service("tokenValidatorFactory")
public class TokenValidatorFactoryImpl implements TokenValidatorFactory { public class TokenValidatorFactoryImpl implements TokenValidatorFactory {
public enum LoginProvider { public enum LoginProvider {
GOOGLE((short) 1), FACEBOOK((short) 2), TWITTER((short) 3), LINKEDIN((short) 4), NATIVELOGIN((short) 5); GOOGLE((short) 1), FACEBOOK((short) 2), TWITTER((short) 3), LINKEDIN((short) 4), NATIVELOGIN((short) 5), B2_ACCESS((short) 6);
private short value; private short value;
@ -35,6 +36,8 @@ public class TokenValidatorFactoryImpl implements TokenValidatorFactory {
return LINKEDIN; return LINKEDIN;
case 5: case 5:
return NATIVELOGIN; return NATIVELOGIN;
case 6:
return B2_ACCESS;
default: default:
throw new RuntimeException("Unsupported LoginProvider"); throw new RuntimeException("Unsupported LoginProvider");
} }
@ -45,13 +48,16 @@ public class TokenValidatorFactoryImpl implements TokenValidatorFactory {
private FacebookTokenValidator facebookTokenValidator; private FacebookTokenValidator facebookTokenValidator;
private LinkedInTokenValidator linkedInTokenValidator; private LinkedInTokenValidator linkedInTokenValidator;
private TwitterTokenValidator twitterTokenValidator; private TwitterTokenValidator twitterTokenValidator;
private B2AccessTokenValidator b2AccessTokenValidator;
@Autowired @Autowired
public TokenValidatorFactoryImpl(GoogleTokenValidator googleTokenValidator, FacebookTokenValidator facebookTokenValidator, LinkedInTokenValidator linkedInTokenValidator, TwitterTokenValidator twitterTokenValidator) { public TokenValidatorFactoryImpl(GoogleTokenValidator googleTokenValidator, FacebookTokenValidator facebookTokenValidator,
LinkedInTokenValidator linkedInTokenValidator, TwitterTokenValidator twitterTokenValidator,B2AccessTokenValidator b2AccessTokenValidator) {
this.googleTokenValidator = googleTokenValidator; this.googleTokenValidator = googleTokenValidator;
this.facebookTokenValidator = facebookTokenValidator; this.facebookTokenValidator = facebookTokenValidator;
this.linkedInTokenValidator = linkedInTokenValidator; this.linkedInTokenValidator = linkedInTokenValidator;
this.twitterTokenValidator = twitterTokenValidator; this.twitterTokenValidator = twitterTokenValidator;
this.b2AccessTokenValidator = b2AccessTokenValidator;
} }
public TokenValidator getProvider(LoginProvider provider) { public TokenValidator getProvider(LoginProvider provider) {
@ -64,6 +70,8 @@ public class TokenValidatorFactoryImpl implements TokenValidatorFactory {
return this.linkedInTokenValidator; return this.linkedInTokenValidator;
case TWITTER: case TWITTER:
return this.twitterTokenValidator; return this.twitterTokenValidator;
case B2_ACCESS:
return this.b2AccessTokenValidator;
default: default:
throw new RuntimeException("Login Provider Not Implemented"); throw new RuntimeException("Login Provider Not Implemented");
} }

View File

@ -0,0 +1,54 @@
package eu.eudat.security.validators.b2access;
import eu.eudat.exceptions.security.NonValidTokenException;
import eu.eudat.models.login.LoginInfo;
import eu.eudat.models.loginprovider.LoginProviderUser;
import eu.eudat.models.security.Principal;
import eu.eudat.security.customproviders.B2AccessCustomProvider;
import eu.eudat.security.customproviders.B2AccessUser;
import eu.eudat.security.validators.TokenValidator;
import eu.eudat.security.validators.b2access.helpers.B2AccessRequest;
import eu.eudat.security.validators.b2access.helpers.B2AccessResponseToken;
import eu.eudat.services.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.security.GeneralSecurityException;
/**
* Created by ikalyvas on 2/22/2018.
*/
@Component("b2AccessTokenValidator ")
public class B2AccessTokenValidator implements TokenValidator {
private B2AccessCustomProvider b2AccessCustomProvider;
private AuthenticationService authenticationService;
private Environment environment;
@Autowired
public B2AccessTokenValidator(AuthenticationService authenticationService, Environment environment, B2AccessCustomProvider b2AccessCustomProvider) {
this.authenticationService = authenticationService;
this.environment = environment;
this.b2AccessCustomProvider = b2AccessCustomProvider;
}
@Override
public Principal validateToken(LoginInfo credentials) throws NonValidTokenException, IOException, GeneralSecurityException {
B2AccessUser b2AccessUser = this.b2AccessCustomProvider.getUser(credentials.getTicket());
LoginProviderUser user = new LoginProviderUser();
user.setId(b2AccessUser.getId());
user.setEmail(b2AccessUser.getEmail());
user.setName(b2AccessUser.getName());
user.setProvider(credentials.getProvider());
user.setSecret(credentials.getTicket());
return this.authenticationService.Touch(user);
}
public B2AccessResponseToken getAccessToken(B2AccessRequest b2AccessRequest) {
return this.b2AccessCustomProvider.getAccessToken(b2AccessRequest.getCode(), this.environment.getProperty("b2access.externallogin.redirect_uri")
, this.environment.getProperty("b2access.externallogin.clientid")
, this.environment.getProperty("b2access.externallogin.clientSecret"));
}
}

View File

@ -0,0 +1,16 @@
package eu.eudat.security.validators.b2access.helpers;
/**
* Created by ikalyvas on 2/22/2018.
*/
public class B2AccessRequest {
private String code;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
}

View File

@ -0,0 +1,16 @@
package eu.eudat.security.validators.b2access.helpers;
/**
* Created by ikalyvas on 2/22/2018.
*/
public class B2AccessResponseToken {
private String accessToken;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
}

View File

@ -44,6 +44,7 @@ public class FacebookTokenValidator implements TokenValidator {
if (profile.getEmail() == null) if (profile.getEmail() == null)
throw new UnauthorisedException("Cannot login user.Facebook account did not provide email"); throw new UnauthorisedException("Cannot login user.Facebook account did not provide email");
user.setEmail(profile.getEmail()); user.setEmail(profile.getEmail());
user.setId(profile.getId());
user.setIsVerified(profile.isVerified()); user.setIsVerified(profile.isVerified());
user.setName(profile.getName()); user.setName(profile.getName());
user.setProvider(TokenValidatorFactoryImpl.LoginProvider.FACEBOOK); user.setProvider(TokenValidatorFactoryImpl.LoginProvider.FACEBOOK);

View File

@ -47,11 +47,11 @@ public class GoogleTokenValidator implements TokenValidator {
@Override @Override
public eu.eudat.models.security.Principal validateToken(LoginInfo credentials) throws NonValidTokenException, IOException, GeneralSecurityException { public eu.eudat.models.security.Principal validateToken(LoginInfo credentials) throws NonValidTokenException, IOException, GeneralSecurityException {
GoogleIdToken idToken = this.verifyUserAndGetUser(credentials.getTicket()); GoogleIdToken idToken = this.verifyUserAndGetUser(credentials.getTicket());
Payload payload = idToken.getPayload(); Payload payload = idToken.getPayload();
LoginProviderUser user = new LoginProviderUser(); LoginProviderUser user = new LoginProviderUser();
user.setSecret(credentials.getTicket()); user.setSecret(credentials.getTicket());
user.setId( payload.getSubject());
user.setProvider(TokenValidatorFactoryImpl.LoginProvider.GOOGLE); user.setProvider(TokenValidatorFactoryImpl.LoginProvider.GOOGLE);
user.setName((String) payload.get("name")); user.setName((String) payload.get("name"));
user.setEmail(payload.getEmail()); user.setEmail(payload.getEmail());

View File

@ -47,6 +47,7 @@ public class LinkedInTokenValidator implements TokenValidator {
if (linkedInProfile.getEmailAddress() == null) if (linkedInProfile.getEmailAddress() == null)
throw new UnauthorisedException("Cannot login user.LinkedIn account did not provide email"); throw new UnauthorisedException("Cannot login user.LinkedIn account did not provide email");
user.setEmail(linkedInProfile.getEmailAddress()); user.setEmail(linkedInProfile.getEmailAddress());
user.setId(linkedInProfile.getId());
user.setIsVerified(true); //TODO user.setIsVerified(true); //TODO
user.setName(linkedInProfile.getFirstName() + " " + linkedInProfile.getLastName()); user.setName(linkedInProfile.getFirstName() + " " + linkedInProfile.getLastName());
user.setProvider(TokenValidatorFactoryImpl.LoginProvider.LINKEDIN); user.setProvider(TokenValidatorFactoryImpl.LoginProvider.LINKEDIN);

View File

@ -54,6 +54,7 @@ public class TwitterTokenValidator implements TokenValidator {
throw new UnauthorisedException("Cannot login user.Twitter account did not provide email"); throw new UnauthorisedException("Cannot login user.Twitter account did not provide email");
user.setEmail((String) values.get("email")); user.setEmail((String) values.get("email"));
user.setIsVerified(true); //TODO user.setIsVerified(true); //TODO
user.setId(""+profile.getId());
user.setName(profile.getName()); user.setName(profile.getName());
user.setProvider(TokenValidatorFactoryImpl.LoginProvider.TWITTER); user.setProvider(TokenValidatorFactoryImpl.LoginProvider.TWITTER);
user.setSecret(finalOauthToken.getValue()); user.setSecret(finalOauthToken.getValue());

View File

@ -91,12 +91,21 @@ public class AuthenticationService {
public Principal Touch(LoginProviderUser profile) { public Principal Touch(LoginProviderUser profile) {
UserInfoCriteria criteria = new UserInfoCriteria(); UserInfoCriteria criteria = new UserInfoCriteria();
criteria.setEmail(profile.getEmail()); criteria.setEmail(profile.getEmail());
UserInfo userInfo = apiContext.getDatabaseRepository().getUserInfoDao().asQueryable().withHint("userInfo").where((builder, root) -> builder.equal(root.get("email"), profile.getEmail())).getSingleOrDefault(); UserInfo userInfo = apiContext.getDatabaseRepository().getUserInfoDao().asQueryable().withHint("userInfo").where((builder, root) -> builder.equal(root.get("email"), profile.getEmail())).getSingleOrDefault();
if(userInfo == null){
Optional<Credential> optionalCredential = Optional.ofNullable(apiContext.getDatabaseRepository().getCredentialDao()
.asQueryable().withHint("credentialUserInfo")
.where((builder, root) -> builder.and(builder.equal(root.get("provider"), profile.getProvider().getValue()), builder.equal(root.get("externalId"), profile.getId())))
.getSingleOrDefault());
userInfo = optionalCredential.map(Credential::getUserInfo).orElse(null);
}
final Credential credential = this.apiContext.getBuilderFactory().getBuilder(CredentialBuilder.class) final Credential credential = this.apiContext.getBuilderFactory().getBuilder(CredentialBuilder.class)
.id(UUID.randomUUID()).creationTime(new Date()).status(1) .id(UUID.randomUUID()).creationTime(new Date()).status(1)
.lastUpdateTime(new Date()).provider((int) profile.getProvider().getValue()) .lastUpdateTime(new Date()).provider((int) profile.getProvider().getValue())
.secret(profile.getSecret()) .secret(profile.getSecret()).externalId(profile.getId())
.build(); .build();
if (userInfo == null) { if (userInfo == null) {

View File

@ -49,4 +49,10 @@ spring.profiles.active=devel
########################Persistence/Hibernate/Connection pool#################### ########################Persistence/Hibernate/Connection pool####################
autouser.root.email=root@dmp.com autouser.root.email=root@dmp.com
autouser.root.password=root autouser.root.password=root
autouser.root.username=root autouser.root.username=root
#################################################################################
b2access.externallogin.user_info_url = https://unity.eudat-aai.fz-juelich.de:443/oauth2/userinfo
b2access.externallogin.access_token_url = https://unity.eudat-aai.fz-juelich.de:443/oauth2/token
b2access.externallogin.redirect_uri = http://dmp.eudat.org:4200/api/oauth/authorized/b2access
b2access.externallogin.clientid = eudatdmptool
b2access.externallogin.clientSecret = A3b*1*92

View File

@ -6,6 +6,7 @@ import { HomepageComponent } from './homepage/homepage.component';
import { AuthGuard } from './guards/auth.guard'; import { AuthGuard } from './guards/auth.guard';
import { LoginComponent } from './user-management/login/login.component'; import { LoginComponent } from './user-management/login/login.component';
import { WelcomepageComponent } from '@app/welcomepage/welcomepage.component'; import { WelcomepageComponent } from '@app/welcomepage/welcomepage.component';
import { B2AccessLoginComponent } from './user-management/login/b2access/b2access-login.component';
const appRoutes: Routes = [ const appRoutes: Routes = [
{ path: 'datasets', loadChildren: './datasets/dataset.module#DatasetModule', canActivate: [AuthGuard] }, { path: 'datasets', loadChildren: './datasets/dataset.module#DatasetModule', canActivate: [AuthGuard] },
@ -17,7 +18,8 @@ const appRoutes: Routes = [
{ path: "unauthorized", loadChildren: './unauthorized/unauthorized.module#UnauthorizedModule' }, { path: "unauthorized", loadChildren: './unauthorized/unauthorized.module#UnauthorizedModule' },
{ path: "users", loadChildren: './users/users.module#UsersModule' }, { path: "users", loadChildren: './users/users.module#UsersModule' },
{ path: "datasetsProfiles", loadChildren: './datasets-admin-listing/dataset-admin.module#DatasetAdminModule' }, { path: "datasetsProfiles", loadChildren: './datasets-admin-listing/dataset-admin.module#DatasetAdminModule' },
{ path: "welcome", component: WelcomepageComponent } { path: "welcome", component: WelcomepageComponent },
{ path: "api/oauth/authorized/b2access", component: B2AccessLoginComponent }
]; ];
@NgModule({ @NgModule({

View File

@ -33,6 +33,7 @@ import { DatasetProfileModule } from './dataset-profile-form/dataset-profile.mod
import { WelcomepageComponent } from '@app/welcomepage/welcomepage.component'; import { WelcomepageComponent } from '@app/welcomepage/welcomepage.component';
import { HelpContentService } from './services/help-content/help-content.service'; import { HelpContentService } from './services/help-content/help-content.service';
import { HelpContentComponent } from './help-content/help-content.component'; import { HelpContentComponent } from './help-content/help-content.component';
import { B2AccessLoginComponent } from './user-management/login/b2access/b2access-login.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -40,7 +41,8 @@ import { HelpContentComponent } from './help-content/help-content.component';
PageNotFoundComponent, PageNotFoundComponent,
HomepageComponent, HomepageComponent,
WelcomepageComponent, WelcomepageComponent,
HelpContentComponent HelpContentComponent,
B2AccessLoginComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -53,7 +55,8 @@ import { HelpContentComponent } from './help-content/help-content.component';
LoginOptions.googleOauth, LoginOptions.googleOauth,
LoginOptions.nativeLogin, LoginOptions.nativeLogin,
LoginOptions.linkedInOauth, LoginOptions.linkedInOauth,
LoginOptions.twitterOauth LoginOptions.twitterOauth,
LoginOptions.b2Access
], ],
facebookConfiguration: { clientId: "110586756143149" }, facebookConfiguration: { clientId: "110586756143149" },
googleConfiguration: { clientId: '524432312250-sc9qsmtmbvlv05r44onl6l93ia3k9deo.apps.googleusercontent.com' }, googleConfiguration: { clientId: '524432312250-sc9qsmtmbvlv05r44onl6l93ia3k9deo.apps.googleusercontent.com' },
@ -64,7 +67,14 @@ import { HelpContentComponent } from './help-content/help-content.component';
accessTokenUri: "https://www.linkedin.com/oauth/v2/accessToken", accessTokenUri: "https://www.linkedin.com/oauth/v2/accessToken",
clientSecret: "2OCO9e3wKylW05Tt" clientSecret: "2OCO9e3wKylW05Tt"
}, },
twitterConfiguration: { clientId: "HiR4hQH9HNubKC5iKQy0l4mAZ", oauthUrl: "https://api.twitter.com/oauth/authenticate" } twitterConfiguration: { clientId: "HiR4hQH9HNubKC5iKQy0l4mAZ", oauthUrl: "https://api.twitter.com/oauth/authenticate" },
b2accessConfiguration: {
clientId: "eudatdmptool",
clientSecret: "A3b*1*92",
oauthUrl: "https://unity.eudat-aai.fz-juelich.de/oauth2-as/oauth2-authz",
redirectUri: "http://dmp.eudat.org:4200/api/oauth/authorized/b2access",
accessTokenUri: "https://unity.eudat-aai.fz-juelich.de:443/oauth2/token"
}
}), }),
HttpModule, HttpModule,
HttpClientModule, HttpClientModule,

View File

@ -2,8 +2,10 @@ export enum LoginProviders {
Google = 1, Google = 1,
Facebook = 2, Facebook = 2,
Twitter = 3, Twitter = 3,
LinkedIn = 4 LinkedIn = 4,
} NativeLogin = 5,
B2Accesss = 6
}
export class LoginInfo { export class LoginInfo {
public ticket: string; public ticket: string;

View File

@ -1,23 +1,22 @@
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<a class="app-title" routerLink="/">{{'NAV-BAR.TITLE' | translate}}</a> <a class="app-title" routerLink="/">{{'NAV-BAR.TITLE' | translate}}</a>
<div *ngIf="isAuthenticated()"> <div *ngIf="isAuthenticated()">
<button mat-button class="navbar-button" routerLink="/projects">{{'NAV-BAR.PROJECTS' | translate}}</button> <button mat-button class="navbar-button" routerLink="/projects">{{'NAV-BAR.PROJECTS' | translate}}</button>
<button mat-button class="navbar-button" routerLink="/dmps">{{'NAV-BAR.DMPS' | translate}}</button> <button mat-button class="navbar-button" routerLink="/dmps">{{'NAV-BAR.DMPS' | translate}}</button>
<button mat-button class="navbar-button" routerLink="/datasets">{{'NAV-BAR.DATASETS' | translate}}</button> <button mat-button class="navbar-button" routerLink="/datasets">{{'NAV-BAR.DATASETS' | translate}}</button>
<button *ngIf="isAdmin()" mat-button class="navbar-button" routerLink="/users">{{'NAV-BAR.USERS' | translate}}</button> <button *ngIf="isAdmin()" mat-button class="navbar-button" routerLink="/users">{{'NAV-BAR.USERS' | translate}}</button>
<button *ngIf="isAdmin()" mat-button class="navbar-button" routerLink="/datasetsProfiles">{{'NAV-BAR.DATASETS(ADMIN)' | translate}}</button> <button *ngIf="isAdmin()" mat-button class="navbar-button" routerLink="/datasetsProfiles">{{'NAV-BAR.DATASETS(ADMIN)' | translate}}</button>
</div> </div>
<span class="navbar-spacer"></span> <span class="navbar-spacer"></span>
<div *ngIf="isAuthenticated();else loginoption"> <div *ngIf="isAuthenticated();else loginoption">
<span class="user-label">{{this.getPrincipalName()}}</span> <span class="user-label">{{this.getPrincipalName()}}</span>
<button mat-icon-button class="navbar-icon" (click)="logout()"> <button mat-icon-button class="navbar-icon" (click)="logout()">
<mat-icon class="navbar-icon">exit_to_app</mat-icon> <mat-icon class="navbar-icon">exit_to_app</mat-icon>
</button> </button>
</div> </div>
<ng-template #loginoption> <ng-template #loginoption>
<button mat-button [routerLink]=" ['/login'] ">
<span class="login-label">Log in</span> <span class="login-label">Log in</span>
<button mat-icon-button class="navbar-icon" [routerLink]=" ['/login'] "> </button>
<mat-icon class="navbar-icon">input</mat-icon> </ng-template>
</button> </mat-toolbar>
</ng-template>
</mat-toolbar>

View File

@ -0,0 +1,26 @@
import { LoginService } from '../../utilties/login-service';
import { Component, OnInit } from '@angular/core'
import { Router, ActivatedRoute, Params } from '@angular/router';
@Component({
selector: 'b2access-login',
templateUrl: './b2access-login.component.html',
})
export class B2AccessLoginComponent implements OnInit {
constructor(
private router: Router,
private route: ActivatedRoute,
private loginService: LoginService
) {
}
ngOnInit(): void {
this.route.queryParams.subscribe((data: any) => {
if (!data["code"]) this.loginService.b2AccessGetAuthCode()
else this.loginService.b2AccessLogin(data["code"])
})
}
}

View File

@ -19,6 +19,14 @@
<i class="fa fa-twitter"></i> <i class="fa fa-twitter"></i>
</button> </button>
</div> </div>
</div>
<div class="card-header">
<div class="social-btns">
<button *ngIf="hasB2AccessOauth()" mat-icon-button (click)="b2AccessLogin()">
<img src="assets/images/b2access.png">
</button>
</div>
</div> </div>
<div *ngIf="hasNativeLogin()"> <div *ngIf="hasNativeLogin()">
<!-- <p class="tip">Or Be Classical</p> --> <!-- <p class="tip">Or Be Classical</p> -->
@ -37,7 +45,7 @@
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<button mat-button (click)="nativeLogin()">LET'S GO</button> <button mat-button (click)="nativeLogin()">LOGIN</button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -45,6 +45,11 @@ export class LoginComponent implements OnInit {
public nativeLogin() { public nativeLogin() {
this.loginService.nativeLogin(this.credential); this.loginService.nativeLogin(this.credential);
} }
public b2AccessLogin() {
return this.loginService.b2AccessInitialiseLogin();
}
public hasFacebookOauth(): boolean { public hasFacebookOauth(): boolean {
return this.loginService.hasProvider(LoginOptions.facebookOauth); return this.loginService.hasProvider(LoginOptions.facebookOauth);
} }
@ -65,4 +70,8 @@ export class LoginComponent implements OnInit {
return this.loginService.hasProvider(LoginOptions.nativeLogin); return this.loginService.hasProvider(LoginOptions.nativeLogin);
} }
public hasB2AccessOauth(): boolean {
return this.loginService.hasProvider(LoginOptions.b2Access);
}
} }

View File

@ -4,5 +4,6 @@ export enum LoginOptions{
twitterOauth = 3, twitterOauth = 3,
googleOauth = 4, googleOauth = 4,
nativeLogin = 5, nativeLogin = 5,
all = 6 b2Access = 6,
all = 7,
} }

View File

@ -17,4 +17,11 @@ export class LinkedInConfiguration extends LoginProviderConfiguration {
public redirectUri: string public redirectUri: string
public accessTokenUri: string public accessTokenUri: string
public clientSecret: string public clientSecret: string
}
export class B2AccessConfiguration extends LoginProviderConfiguration {
public oauthUrl: string
public redirectUri: string
public accessTokenUri: string
public clientSecret: string
} }

View File

@ -3,6 +3,7 @@ import {
GoogleLoginConfiguration, GoogleLoginConfiguration,
LinkedInConfiguration, LinkedInConfiguration,
TwitterLoginConfiguration, TwitterLoginConfiguration,
B2AccessConfiguration,
} from './LoginProviderConfiguration'; } from './LoginProviderConfiguration';
import { LoginOptions } from './LoginOptions'; import { LoginOptions } from './LoginOptions';
export class LoginServiceConfiguration { export class LoginServiceConfiguration {
@ -11,4 +12,5 @@ export class LoginServiceConfiguration {
public googleConfiguration?: GoogleLoginConfiguration; public googleConfiguration?: GoogleLoginConfiguration;
public twitterConfiguration?: TwitterLoginConfiguration; public twitterConfiguration?: TwitterLoginConfiguration;
public linkedInConfiguration?: LinkedInConfiguration; public linkedInConfiguration?: LinkedInConfiguration;
public b2accessConfiguration?: B2AccessConfiguration;
} }

View File

@ -56,6 +56,7 @@ export class LoginService {
case LoginOptions.googleOauth: return this.hasAllRequiredFieldsConfigured(this.config.googleConfiguration) case LoginOptions.googleOauth: return this.hasAllRequiredFieldsConfigured(this.config.googleConfiguration)
case LoginOptions.linkedInOauth: return this.hasAllRequiredFieldsConfigured(this.config.linkedInConfiguration); case LoginOptions.linkedInOauth: return this.hasAllRequiredFieldsConfigured(this.config.linkedInConfiguration);
case LoginOptions.twitterOauth: return this.hasAllRequiredFieldsConfigured(this.config.twitterConfiguration); case LoginOptions.twitterOauth: return this.hasAllRequiredFieldsConfigured(this.config.twitterConfiguration);
case LoginOptions.b2Access: return this.hasAllRequiredFieldsConfigured(this.config.b2accessConfiguration);
case LoginOptions.nativeLogin: return true; case LoginOptions.nativeLogin: return true;
default: throw new Error("Unsupported Provider Type") default: throw new Error("Unsupported Provider Type")
} }
@ -169,6 +170,31 @@ export class LoginService {
) )
} }
/*
* B2ACCESS LOG IN
*/
public b2AccessInitialiseLogin() {
this.router.navigate(["/api/oauth/authorized/b2access"])
}
public b2AccessGetAuthCode() {
window.location.href = this.config.b2accessConfiguration.oauthUrl + "?response_type=code&client_id=" + this.config.b2accessConfiguration.clientId + "&redirect_uri=" + this.config.b2accessConfiguration.redirectUri + "&state=987654321&scope=USER_PROFILE"
}
public b2AccessLogin(code: String) {
let headers = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json');
headers = headers.set('Accept', 'application/json');
this.httpClient.post(HostConfiguration.Server + "auth/b2AccessRequestToken", { code: code }, { headers: headers })
.subscribe((data: any) => {
this.authService.login({ ticket: data.payload.accessToken, provider: LoginProviders.B2Accesss, data: null }).subscribe(
res => this.onLogInSuccess(res),
error => this.onLogInError(error)
)
})
}
/* /*
* NATIVE LOGIN * NATIVE LOGIN

View File

@ -2,6 +2,8 @@
<html> <html>
<head> <head>
<script src="https://apis.google.com/js/platform.js"></script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta http-equiv="content-type" content="text/html; charset=utf-8" />
@ -12,14 +14,14 @@
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<script src="//connect.facebook.net/en_US/all.js"></script> <script src="//connect.facebook.net/en_US/all.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script src="https://apis.google.com/js/platform.js" async defer></script>
<script type="text/javascript" src="//platform.linkedin.com/in.js"> <script type="text/javascript" src="//platform.linkedin.com/in.js">
api_key: 86bl8vfk77clh9 api_key: 86bl8vfk77clh9
</script> </script>
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
</head> </head>