-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(jans-auth-server): #808 sign-in with apple interception script
- Loading branch information
Showing
2 changed files
with
396 additions
and
0 deletions.
There are no files selected for viewing
197 changes: 197 additions & 0 deletions
197
jans-auth-server/server/src/main/webapp/auth/apple/login.xhtml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | ||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | ||
<ui:composition xmlns="http://www.w3.org/1999/xhtml" | ||
xmlns:f="http://xmlns.jcp.org/jsf/core" | ||
xmlns:ui="http://xmlns.jcp.org/jsf/facelets" | ||
xmlns:h="http://xmlns.jcp.org/jsf/html" | ||
template="/WEB-INF/incl/layout/login-template.xhtml"> | ||
<f:metadata> | ||
<f:viewAction action="#{authenticator.prepareAuthenticationForStep}" /> | ||
<f:viewParam name="login_hint" value="#{authorizeAction.loginHint}" /> | ||
</f:metadata> | ||
<ui:define name="head"> | ||
<meta name="description" content="Gluu, Inc." /> | ||
</ui:define> | ||
<ui:define name="pageTitle"> | ||
<h:outputText value="#{msgs['login.pageTitle']}" /> | ||
</ui:define> | ||
<ui:define name="body"> | ||
<script type="text/javascript" src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script> | ||
|
||
<div class="container"> | ||
<h:panelGroup rendered="true"> | ||
<div class="login_bx_1" | ||
style="border-radius: 10px; margin-top: 0px; background: white; border: 1px solid #008b8b;"> | ||
<div class="row"> | ||
<h:messages class="text-center" | ||
style="color:#8b0000;margin:5px;margin-left:20px; font-size:2vw;" | ||
infoClass="text-center" errorClass="text-center" /> | ||
</div> | ||
<h:form id="loginForm" style="padding:30px;"> | ||
<div class="row"> | ||
<div class="col-sm-3 col-md-3"> | ||
<h:outputText value="#{msgs['login.username']}" /> | ||
</div> | ||
<div class="col-sm-9 col-md-9"> | ||
<h:inputText placeholder="#{msgs['login.username']}" | ||
id="username" name="username" required="true" colMd="10" | ||
labelColMd="2" autocomplete="off" | ||
value="#{credentials.username}" styleClass="form-control" | ||
style="width:100%"> | ||
</h:inputText> | ||
</div> | ||
</div> | ||
<div class="form-group row"></div> | ||
<div class="row"> | ||
<div class="col-sm-3 col-md-3"> | ||
<h:outputText value="#{msgs['login.password']}" /> | ||
</div> | ||
<div class="col-sm-9 col-md-9"> | ||
<h:inputSecret placeholder="#{msgs['login.password']}" | ||
colMd="10" id="password" name="password" labelColMd="2" | ||
value="#{credentials.password}" autocomplete="off" | ||
styleClass="form-control" tyle="width:100%"> | ||
</h:inputSecret> | ||
</div> | ||
</div> | ||
<div class="form-group row"></div> | ||
|
||
<div class="row"> | ||
<div class="col-sm-5 col-md-5"> | ||
<h:outputLabel styleClass="col-form-label" for="rememberme" | ||
value="#{msgs['login.rememberMe']}" /> | ||
</div> | ||
<div class="col-sm-2 col-md-2" style="padding-top: 15px"> | ||
<input type="checkbox" value="rememberme" id="rememberme" | ||
name="rememberme" /> | ||
</div> | ||
</div> | ||
<div class="form-group row"> | ||
<div class="col-sm-offset-2 offset-md-2 col-sm-8 col-md-8"> | ||
<h:commandButton id="loginButton" | ||
style="background-color: #00BE79; color:white;" | ||
styleClass="btn col-sm-12" value=" #{msgs['login.login']}" | ||
onclick="checkRemembeMe()" iconAwesome="fa-sign-in" | ||
action="#{authenticator.authenticate}" /> | ||
</div> | ||
</div> | ||
<div class="form-group row"> | ||
<div class="col-sm-offset-3 offset-md-3 col-sm-7 col-md-7"> | ||
<div class="forgot_link"> | ||
<a href="/identity/person/passwordReminder.htm" | ||
style="color: blue;"> <h:outputText | ||
value="#{msgs['login.forgotYourPassword']}" /> | ||
</a> | ||
</div> | ||
</div> | ||
</div> | ||
<h:inputHidden id="platform" /> | ||
<div id="appleid-signin" data-color="black" data-border="true" data-type="sign in"></div> | ||
|
||
</h:form> | ||
<div class="row"> | ||
<div align="center" class="col-sm-offset-4 col-sm-9"> | ||
<ui:repeat | ||
value="#{identity.getWorkingParameter('download_url').entrySet().toArray()}" | ||
var="_entry"> | ||
<ui:param name="app_name" value="#{_entry.key}" /> | ||
<ui:param name="app_link" value="#{_entry.value}" /> | ||
<h:outputLink style="margin-right:5px;" value="#{app_link}" | ||
title="#{app_name}"> | ||
<h:graphicImage value="img/#{app_name}.png" /> | ||
</h:outputLink> | ||
</ui:repeat> | ||
</div> | ||
<h:panelGroup layout="block" | ||
rendered="#{external_registration_uri != Null}"> | ||
<div class="reg_link"> | ||
<a href="#{external_registration_uri}"> <h:outputText | ||
value="Register Now " /> | ||
</a> | ||
</div> | ||
</h:panelGroup> | ||
</div> | ||
</div> | ||
|
||
</h:panelGroup> | ||
</div> | ||
<script type="text/javascript"> | ||
|
||
|
||
|
||
apple_init_obj = {}; | ||
apple_init_obj['clientId'] = `${identity.getWorkingParameter('aclient_id')}`; | ||
apple_init_obj['scope'] = 'name email'; | ||
apple_init_obj['redirectURI'] = encodeURI("${identity.getWorkingParameter('aredirectURI')}"); | ||
apple_init_obj['astate'] = `${identity.getWorkingParameter('astate')}`; | ||
apple_init_obj['anonce'] = `${identity.getWorkingParameter('anonce')}`; | ||
|
||
//apple_init_obj['usePopup'] = true; | ||
|
||
AppleID.auth.init(apple_init_obj); | ||
</script> | ||
<script type="text/javascript"> | ||
$(document).ready(function () { | ||
|
||
if (localStorage.chkbx && localStorage.chkbx != '') { | ||
$('#rememberme').attr('checked', 'checked') | ||
document.getElementById("loginForm:username").value = localStorage.usrname; | ||
} else { | ||
$('#rememberme').removeAttr('checked'); | ||
document.getElementById("loginForm:username").value = ""; | ||
} | ||
|
||
$('#rememberme').click(function() { | ||
checkRemembeMe(); | ||
}); | ||
|
||
fillPlatformField(); | ||
|
||
var userNameField = document.getElementById("loginForm:username"); | ||
var passwordField = document.getElementById("loginForm:password"); | ||
|
||
passwordField.value = ""; | ||
var userName = '#{!empty authorizeAction.loginHint ? authorizeAction.loginHint : ""}'; | ||
if (userName) { | ||
userNameField.value = userName; | ||
passwordField.focus(); | ||
} else { | ||
userNameField.focus(); | ||
} | ||
|
||
var displayRegister = #{display_register_action or identity.sessionId.sessionAttributes['display_register_action']}; | ||
if (displayRegister) { | ||
var registerButton = document.getElementById("loginForm:registerId"); | ||
if (registerButton != null) { | ||
registerButton.style.display = 'inline'; | ||
} | ||
} | ||
}); | ||
|
||
function checkRemembeMe() { | ||
if ($('#rememberme').is(':checked')) { | ||
localStorage.usrname = document.getElementById("loginForm:username").value; | ||
localStorage.chkbx = $('#rememberme').val(); | ||
} else { | ||
localStorage.usrname = ''; | ||
localStorage.chkbx = ''; | ||
} | ||
} | ||
|
||
function fillPlatformField() { | ||
try { | ||
re = /^([^\.]+\.[^\.]+)\..+/; | ||
result = re.exec(platform.version); | ||
if (result != null) { | ||
platform.version=result[1]; | ||
} | ||
document.getElementById("loginForm:platform").value = JSON.stringify(platform); | ||
} catch (e) { | ||
} | ||
} | ||
|
||
|
||
</script> | ||
|
||
</ui:define> | ||
</ui:composition> |
199 changes: 199 additions & 0 deletions
199
...nux-setup/jans_setup/static/extension/person_authentication/AppleExternalAuthenticator.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
# Janssen Project software is available under the Apache 2.0 License (2004). See http://www.apache.org/licenses/ for full text. | ||
# Copyright (c) 2020, Janssen Project | ||
# | ||
# Author: Madhumita Subramaniam | ||
# | ||
|
||
from io.jans.as.server.util import ServerUtil | ||
from io.jans.service.cdi.util import CdiUtil | ||
from io.jans.model.security import Identity | ||
from io.jans.as.common.model.common import User | ||
from jakarta.faces.context import FacesContext | ||
from io.jans.as.server.service.net import HttpService | ||
from io.jans.model.custom.script.type.auth import PersonAuthenticationType | ||
from io.jans.as.server.service import AuthenticationService, UserService | ||
from io.jans.orm import PersistenceEntryManager | ||
from io.jans.as.persistence.model.configuration import GluuConfiguration | ||
from io.jans.util import StringHelper | ||
from java.math import BigInteger | ||
from java.security import SecureRandom | ||
import java | ||
import sys | ||
import json | ||
from java.util import UUID | ||
from io.jans.as.model.jws import ECDSASigner; | ||
from io.jans.as.model.jws import RSASigner; | ||
from io.jans.as.model.jwt import Jwt; | ||
from io.jans.as.model.crypto.signature import ECDSAPublicKey; | ||
from io.jans.as.model.crypto.signature import RSAPublicKey; | ||
from io.jans.as.client import JwkClient; | ||
from io.jans.as.model.crypto.signature import AlgorithmFamily; | ||
from java.util import Collections, HashMap, HashSet, ArrayList, Arrays, Date | ||
|
||
class PersonAuthentication(PersonAuthenticationType): | ||
def __init__(self, currentTimeMillis): | ||
self.currentTimeMillis = currentTimeMillis | ||
|
||
def init(self, customScript, configurationAttributes): | ||
print "Apple Initialization" | ||
if (not configurationAttributes.containsKey("apple_client_id")): | ||
print "Apple. Initialization. Property apple_client_id is not specified" | ||
return False | ||
else: | ||
self.apple_client_id = configurationAttributes.get("apple_client_id").getValue2() | ||
|
||
if (not configurationAttributes.containsKey("apple_jwks")): | ||
print "Apple. Initialization. Property apple_jwks is not specified" | ||
return False | ||
else: | ||
self.apple_jwks = configurationAttributes.get("apple_jwks").getValue2() | ||
print "Apple Initialized successfully" | ||
return True | ||
|
||
def destroy(self, configurationAttributes): | ||
print "Apple Destroy" | ||
print "Apple Destroyed successfully" | ||
return True | ||
|
||
def getAuthenticationMethodClaims(self, requestParameters): | ||
return None | ||
|
||
def getApiVersion(self): | ||
return 11 | ||
|
||
def isValidAuthenticationMethod(self, usageType, configurationAttributes): | ||
return True | ||
|
||
def getAlternativeAuthenticationMethod(self, usageType, configurationAttributes): | ||
return None | ||
|
||
|
||
def authenticate(self, configurationAttributes, requestParameters, step): | ||
authenticationService = CdiUtil.bean(AuthenticationService) | ||
|
||
if (step == 1): | ||
print "Apple Authenticate for step 1" | ||
identity = CdiUtil.bean(Identity) | ||
print requestParameters | ||
|
||
id_token = ServerUtil.getFirstValue(requestParameters, "id_token") | ||
if id_token is not None: | ||
|
||
apple_id = self.verifyIDToken(id_token) | ||
print apple_id | ||
# if user doesnt exist in persistence, add | ||
foundUser = self.findUserByAppleId(apple_id) | ||
print foundUser | ||
if foundUser is None: | ||
foundUser = User() | ||
foundUser.setAttribute("jansExtUid", "passport-apple:"+apple_id) | ||
foundUser.setAttribute(self.getLocalPrimaryKey(),apple_id) | ||
|
||
userService = CdiUtil.bean(UserService) | ||
result = userService.addUser(foundUser, True) | ||
foundUser = self.findUserByAppleId(apple_id) | ||
|
||
|
||
logged_in = authenticationService.authenticate(apple_id) | ||
return logged_in | ||
|
||
else: | ||
credentials = identity.getCredentials() | ||
user_name = credentials.getUsername() | ||
user_password = credentials.getPassword() | ||
|
||
logged_in = False | ||
if (StringHelper.isNotEmptyString(user_name) and StringHelper.isNotEmptyString(user_password)): | ||
logged_in = authenticationService.authenticate(user_name, user_password) | ||
|
||
return logged_in | ||
else: | ||
print "Apple Authenticate Error" | ||
return False | ||
|
||
def verifyIDToken(self, appleIdToken): | ||
|
||
if appleIdToken is not None: | ||
jwt = Jwt.parse(appleIdToken) | ||
algorithm = jwt.getHeader().getSignatureAlgorithm() | ||
keyId = jwt.getHeader().getKeyId(); | ||
userId = jwt.getClaims().getClaimAsString("sub") | ||
print userId | ||
jwksURI = self.apple_jwks; | ||
|
||
validSignature = False; | ||
if (algorithm.getFamily() == AlgorithmFamily.RSA) : | ||
publicKey = JwkClient.getRSAPublicKey(jwksURI, keyId); | ||
rsaSigner = RSASigner(algorithm, publicKey); | ||
validSignature = rsaSigner.validate(jwt); | ||
elif (algorithm.getFamily() == AlgorithmFamily.EC) : | ||
publicKey = JwkClient.getECDSAPublicKey(jwksURI, keyId); | ||
ecdsaSigner = ECDSASigner(algorithm, publicKey); | ||
validSignature = ecdsaSigner.validate(jwt); | ||
|
||
if(validSignature is True): | ||
return userId | ||
else: | ||
print "Apple. Failed to validate signature of ID_token" | ||
return None | ||
|
||
else : | ||
print "Invalid ID token." | ||
return None | ||
|
||
def findUserByAppleId(self, apple_id): | ||
userService = CdiUtil.bean(UserService) | ||
return userService.getUserByAttribute("jansExtUid", "passport-apple:"+apple_id) | ||
|
||
def getLocalPrimaryKey(self): | ||
entryManager = CdiUtil.bean(PersistenceEntryManager) | ||
config = GluuConfiguration() | ||
config = entryManager.find(config.getClass(), "ou=configuration,o=jans") | ||
#Pick (one) attribute where user id is stored (e.g. uid/mail) | ||
# primaryKey is the primary key on the backend AD / LDAP Server | ||
# localPrimaryKey is the primary key on Janssen. This attr value has been mapped with the primary key attr of the backend AD / LDAP when configuring cache refresh | ||
uid_attr = config.getIdpAuthn().get(0).getConfig().findValue("localPrimaryKey").asText() | ||
print "Casa. init. uid attribute is '%s'" % uid_attr | ||
return | ||
|
||
def prepareForStep(self, configurationAttributes, requestParameters, step): | ||
if (step == 1): | ||
print "Apple Prepare for Step 1" | ||
identity = CdiUtil.bean(Identity) | ||
identity.setWorkingParameter("aclient_id",self.apple_client_id) | ||
identity.setWorkingParameter("astate", UUID.randomUUID().toString()) | ||
identity.setWorkingParameter("anonce", UUID.randomUUID().toString()) | ||
|
||
facesContext = CdiUtil.bean(FacesContext) | ||
request = facesContext.getExternalContext().getRequest() | ||
httpService = CdiUtil.bean(HttpService) | ||
url = httpService.constructServerUrl(request) + "/postlogin.htm" | ||
|
||
identity.setWorkingParameter("aredirectURI",url) | ||
return True | ||
else: | ||
return False | ||
|
||
def getExtraParametersForStep(self, configurationAttributes, step): | ||
list = ArrayList() | ||
list.addAll(Arrays.asList("apple_client_id","astate","anonce")) | ||
return list | ||
|
||
def getCountAuthenticationSteps(self, configurationAttributes): | ||
return 1 | ||
|
||
def getPageForStep(self, configurationAttributes, step): | ||
|
||
if(step == 1): | ||
return "/auth/apple/login.xhtml" | ||
return "" | ||
|
||
def getNextStep(self, configurationAttributes, requestParameters, step): | ||
return -1 | ||
|
||
def getLogoutExternalUrl(self, configurationAttributes, requestParameters): | ||
print "Get external logout URL call" | ||
return None | ||
|
||
def logout(self, configurationAttributes, requestParameters): | ||
return True |