Sign In with Apple, decoded Apple response - ios

I've implemented 'Sign In with Apple' from this source (https://gist.github.com/aamishbaloch/2f0e5d94055e1c29c0585d2f79a8634e?permalink_comment_id=3328115) taking into account the comments of NipunShaji and aj3sh. But it doesn't works because Apple sends incomplete data: I recieve
decoded = {'iss': 'https://appleid.apple.com', 'aud': '...', 'exp': 1664463442, 'iat': 1664377042, 'sub': '.....', 'at_hash': '....', 'auth_time': 1664377030, 'nonce_supported': True}
without email data).
According to the Apple's documentation typical response contains email: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple.
What I've missed?
Additional code:
view.py file:
class AppleSocialAuthView(GenericAPIView):
serializer_class = AppleSocialAuthSerializer
permission_classes = [AllowAny]
def post(self, request):
"""
POST with "auth_token"
Send an access token as from facebook to get user information
"""
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
data = (serializer.validated_data['auth_token'])
return Response(data, status=status.HTTP_200_OK)
serializer.py file:
class Apple(BaseOAuth2):
"""apple authentication backend"""
name = 'apple'
ACCESS_TOKEN_URL = 'https://appleid.apple.com/auth/token'
SCOPE_SEPARATOR = ','
ID_KEY = 'uid'
#handle_http_errors
def do_auth(self, access_token, *args, **kwargs):
"""
Finish the auth process once the access_token was retrieved
Get the email from ID token received from apple
"""
response_data = {}
client_id, client_secret = self.get_key_and_secret()
headers = {'content-type': "application/x-www-form-urlencoded"}
data = {
'client_id': client_id,
'client_secret': client_secret,
'code': access_token,
'grant_type': 'authorization_code',
'redirect_uri': settings.SOCIAL_AUTH_APPLE_REDIRECT_URL
}
res = requests.post(Apple.ACCESS_TOKEN_URL, data=data, headers=headers)
response_dict = res.json()
id_token = response_dict.get('id_token', None)
if id_token:
decoded = jwt.decode(id_token, '', options={"verify_signature": False}, verify=False)
print(decoded)
response_data.update({'email': decoded['email']}) if 'email' in decoded else None
response_data.update({'uid': decoded['sub']}) if 'sub' in decoded else None
response = kwargs.get('response') or {}
response.update(response_data)
response.update({'access_token': access_token}) if 'access_token' not in response else None
kwargs.update({'response': response, 'backend': self})
return self.strategy.authenticate(*args, **kwargs)
def get_user_details(self, response):
email = response.get('email', None)
details = {
'email': email,
}
return details
def get_key_and_secret(self):
headers = {
'kid': settings.SOCIAL_AUTH_APPLE_KEY_ID,
'alg': 'ES256',
}
payload = {
'iss': settings.SOCIAL_AUTH_APPLE_TEAM_ID,
'iat': int(time.time()),
'exp': int(time.time()) + 15552000,
'aud': 'https://appleid.apple.com',
'sub': settings.SOCIAL_AUTH_APPLE_CLIENT_ID,
}
client_secret = jwt.encode(
payload,
settings.SOCIAL_AUTH_APPLE_CLIENT_SECRET,
# algorithm='ES256',
headers=headers
)
return settings.SOCIAL_AUTH_APPLE_CLIENT_ID, client_secret
class AppleSocialAuthSerializer(serializers.Serializer):
auth_token = serializers.CharField()
def validate_auth_token(self, auth_token):
user_data = Apple()
user_data = user_data.do_auth(auth_token)
try:
email = user_data['email']
name = user_data['name']
provider = 'apple'
return register_social_user(
provider=provider, email=email, name=name)
except Exception as identifier:
raise serializers.ValidationError(
'The token is invalid or expired. Please login again.'
)

When I test this proces on my Mac (logging into web app), the end result is that I can see on my Mac, preferences -> Apple ID, that I'm using SSO for this application.
So it looks like Apple validated this Web App.
If they do send email, only first time the user is logging in to Web App, how Web App should know next time what user to log in?
There is no single parameter that would identify the user in decoded response (like some ID, which would also appear in their first response?
Best Regards, Marek

Related

Apache Superset and Auth0 returns "The browser (or proxy) sent a request that this server could not understand."

I'm trying to set up Superset with Auth0. I've found somewhat similar issues here and here.
I've set up the following configuration based on the first link above and trying to follow the Superset and Flask-AppBuilder docs:
from flask_appbuilder.security.manager import (
AUTH_OAUTH,
)
from superset.security import SupersetSecurityManager
import json
import logging
import string
import random
nonce = ''.join(random.choices(string.ascii_uppercase + string.digits + string.ascii_lowercase, k = 30))
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
AUTH_TYPE = AUTH_OAUTH
AUTH_USER_REGISTRATION = True
AUTH_USER_REGISTRATION_ROLE = "Admin"
AUTH0_URL = os.getenv('AUTH0_URL')
AUTH0_CLIENT_KEY = os.getenv('AUTH0_CLIENT_KEY')
AUTH0_CLIENT_SECRET = os.getenv('AUTH0_CLIENT_SECRET')
OAUTH_PROVIDERS = [
{ 'name':'auth0',
'token_key':'access_token',
'icon':'fa-at',
'remote_app': {
'api_base_url': AUTH0_URL,
'client_id': AUTH0_CLIENT_KEY,
'client_secret': AUTH0_CLIENT_SECRET,
'server_metadata_url': AUTH0_URL + '/.well-known/openid-configuration',
'client_kwargs': {
'scope': 'openid profile email'
},
'response_type': 'code token',
'nonce': nonce,
}
}
]
class CustomSsoSecurityManager(SupersetSecurityManager):
def oauth_user_info(self, provider, response=None):
logger.debug('oauth2 provider: {0}'.format(provider))
if provider == 'auth0':
res = self.appbuilder.sm.oauth_remotes[provider].get(AUTH0_URL + '/userinfo')
logger.debug('response: {0}'.format(res))
if res.raw.status != 200:
logger.error('Failed to obtain user info: %s', res.json())
return
# user_info = self.appbuilder.sm.oauth_remotes[provider].parse_id_token(res)
# logger.debug('user_info: {0}'.format(user_info))
me = res.json()
return {
'username' : me['email'],
'name' : me['name'],
'email' : me['email'],
}
CUSTOM_SECURITY_MANAGER = CustomSsoSecurityManager
The full error log message is:
2022-03-18 18:53:56,854:ERROR:flask_appbuilder.security.views:Error authorizing OAuth access token: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
NOTES:
I can see an access_token parameter in the redirect url, so it seems to be working with Auth0 correctly.
I don't see any of the debug lines in the CustomSsoSecurityManager being written, so my guess is that I have not correctly set that up (or my logging is not correctly configured).
I've tried using both Regular Web Application and Single Page Application application types in Auth0, and both fail in the same way.
I would appreciate any help in understanding what I might be missing or what else I need to do to configure Auth0 to work with Superset.
I was able to make it work using the JSON Web Key Set endpoint provided by Auth0, look at this example and adapt it accordingly:
from jose import jwt
from requests import request
from superset.security import SupersetSecurityManager
class CustomSecurityManager(SupersetSecurityManager):
def request(self, url, method="GET", *args, **kwargs):
kwargs.setdefault("headers", {})
response = request(method, url, *args, **kwargs)
response.raise_for_status()
return response
def get_jwks(self, url, *args, **kwargs):
return self.request(url, *args, **kwargs).json()
def get_oauth_user_info(self, provider, response=None):
if provider == "auth0":
id_token = response["id_token"]
metadata = self.appbuilder.sm.oauth_remotes[provider].server_metadata
jwks = self.get_jwks(metadata["jwks_uri"])
audience = self.appbuilder.sm.oauth_remotes[provider].client_id
payload = jwt.decode(
id_token,
jwks,
algorithms=["RS256"],
audience=audience,
issuer=metadata["issuer"],
)
first_name, last_name = payload["name"].split(" ", 1)
return {
"email": payload["email"],
"username": payload["email"],
"first_name": first_name,
"last_name": last_name,
}
return super().get_oauth_user_info(provider, response)

I want to send the proper parameter to salesforce for authentication with JWT on Ruby

I am currently working on the project that I am going to integrate the application of my company and salesforce.
In my case, it seemed that using the JWT for authentication is better. So, I wanted to try it.
but I don't know how to generate JWT and send the proper request to salesforce on Ruby though I read docs.
What I wanted to do is that
1, create application on salesforce (done)
2, create X509 certification and set it on the application on salesforce. (done)
3, create JWT by using the secret key of X509 certification. (I think I've done it )
4, send post request with JWT parameter included in assertion params and grant_type(grant_type= urn:ietf:params:oauth:grant-type:jwt-bearer&) (I got an error)
when I send post request the errors says {"error":"invalid_grant","error_description":"invalid assertion"} so it occurs certainly because of the parameter I sent.
the code I randomly wrote is something like this.
require 'jwt'
require 'json'
require 'net/http'
require 'uri'
payload = {
"sub": "abel#example.com", ← my account on salesforce
"iss": "3MVG9pe2TCoA1PasbdvjabsodyoQFZTn0Rjsdbfjbasojdbn;oajs", ← the consumer key of the application on salesforce.
"aud": "https://test.salesforce.com"
}
public_key = Base64.urlsafe_encode64(
'USqTxNC7MMIeF9iegop3WeDvFL
075JSUECgYEA76FNJLeStdq+J6Fj2ZBYdDoeuDHv3iNA0nnIse9d6HnjbdrdvjmV
rT1CJuHh9gnNKg4tyjkbpc9IVj4/GF0mNUCgYEAynvj
qOYCzts4W7Bdumk6z8QULJ5QoYCrAgFtwra9R1HDcxTz+GPgJOVx2QBX+aQbDOaD
WV1s9WqE0/Lfi/VVUEzg1hZ8326buGRk1DRVG2Oa48==') ← this is public_key example of the certification.
rsa_private = OpenSSL::PKey::RSA.generate 2048
rsa_public = rsa_private.public_key
token = JWT.encode payload, rsa_private, 'RS256'
puts token
decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
puts decoded_token
post = {
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion': token
}
uri = URI.parse('https://login.salesforce.com/services/oauth2/token')
https = Net::HTTP.new(uri.host, 443)
https.use_ssl = true
response = https.post(uri.path, post.to_query)
print response.body
the PHP version of what I want to achieve is something like this.
<?php
require_once './vendor/autoload.php';
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Rsa\Sha256;
// login URL
// production: https://login.salesforce.com
// Sandbox: https://test.login.salesforce.com
define('LOGIN_URL', 'https://test.salesforce.com');
//consumer key
define('CLIENT_ID', <<consumer key of the application on salesforce>>);
//user ID
define('USER_ID', 'xxxxx#example.com');
function createjwt() {
$signer = new Sha256();
$privateKey = new Key('file://cert/server.key'); ← probably the key from certification
$time = time();
$token = (new Builder())->issuedBy(CLIENT_ID) // iss: consumer key
->permittedFor(LOGIN_URL) // aud: Salesforce login URL
->relatedTo(USER_ID) // sub: Salesforce user ID
->expiresAt($time + 3 * 60) // exp: within three mins
->getToken($signer, $privateKey);
return $token;
}
$jwt = createjwt();
echo $jwt;
function auth() {
$jwt = createjwt();
$post = array(
'grant_type' => GRANT_TYPE,
'assertion' => $jwt,
);
$curl = curl_init();
curl_setopt( $curl, CURLOPT_URL, AUTH_URL );
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 );
curl_setopt( $curl, CURLOPT_POSTFIELDS, $post );
$buf = curl_exec( $curl );
if ( curl_errno( $curl ) ) {
exit;
}
curl_close( $curl );
$json = json_decode( $buf );
$accinfo = array(
// URL to access
'instance_url' => $json->instance_url,
// Bearer token in order to access
'access_token' => $json->access_token,
);
return $accinfo;
}
$accinfo = auth();
EDIT
I changed a code a lot. But I still have different error that says 'initialize': Neither PUB key nor PRIV key: nested asn1 error (OpenSSL::PKey::RSAError)' around #private_key definition.
I read this and tried changing the string in private_key.pem to in one line but I didn't work ( maybe I did in a wrong way) and didn't understand the meaning of incorrect password (mentioned as the second answer) What causes "Neither PUB key nor PRIV key:: nested asn1 error" when building a public key in ruby?
def initialize
#cert_file = File.join(File.dirname(__FILE__), *%w[private_key.pem])
# #cert = Base64.urlsafe_encode64(#cert_file)
# print #cert_
# #cert_file = File.join(File.dirname(__FILE__), *%w[server.csr])
#base_url = "https://test.salesforce.com"
#auth_endpoint = "/services/oauth2/authorize"
#token_request_endpoint = "/services/oauth2/token"
#token_revoke_endpoint = "/services/oauth2/revoke"
#username = "my username"
#client_id = "pe2TCoA1~~~~" client_id
#private_key = OpenSSL::PKey::RSA.new(File.read(#cert_file))
# #private_key = OpenSSL::PKey::RSA.generate(private_key)
#rsa_public = #private_key.public_key
# #private_key = OpenSSL::PKey::RSA.new(File.read(#cert_file))
end
def claim_set
{
iss: #client_id,
sub: #username,
aud: #base_url,
exp: (Time.now + 3.minutes).to_i.to_s
}
end
def jwt_bearer_token
JWT.encode(self.claim_set.to_s, #rsa_public, 'RS256')
end
def request_auth
post = {body: {grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt_bearer_token}}
uri = URI.parse("#{#base_url}#{#token_request_endpoint}")
https = Net::HTTP.new(uri.host, 443)
https.use_ssl = true
response = https.post(uri.path, post.to_query)
print response.body
end
Salesforce.new.request_auth
end
Any advices are appreciated.
Thank you

The view function did not return a valid response. The return type must be a string, dict

using Spotify API and Flask I am trying to extend refresh_token validity. As a result, when I send a request to the server, I get this error:
*The view function did not return a valid response. The return type must be a string, dict, tuple, Response instance, or WSGI callable, but it was a Response.*
My code:
#app.route("/index")
def index():
if time.time() > session['expires_in']:
payload = session['refresh_token']
ref_payload = {
'grant_type': 'refresh_token',
'refresh_token':session["refresh_token"]
}
header={'Authorization': 'Basic ' + '<CLIENT_ID>:<CLIENT_SECRET'}
r = requests.post(AUTH_URL, data=ref_payload, headers=header)
return r
#app.route("/q")
def api_callback():
session.clear()
code = request.args.get('code')
res = requests.post(AUTH_URL, data={
"grant_type":"authorization_code",
"code":code,
"redirect_uri":REDIRECT_URI,
"client_id":CLIENT_ID,
"client_secret":CLIENT_SECRET
})
res_body = res.json()
session["token"] = res_body.get("access_token")#token
session["expires_in"] = res_body.get("expires_in")#time
session["refresh_token"] = res_body.get("refresh_token")#reflesh token
return redirect("index")
https://accounts.spotify.com/api/token is accepted as AUTH_URL
Most likely the problem is very commonplace, but I can't think of a solution now. Thanks in advance
I solved this problem. In my configurashion file i was create a veriable in which i encode my client_id and client_secret to base64 format:
ID_SEC = CLIENT_ID +':'+ CLIENT_SECRET
base64_encode = base64.b64encode(ID_SEC.encode()).decode()
After in the header i edit authorisation :
header={
'Content-Type':'application/x-www-form-urlencoded',
'Accept': 'application/json',
'Authorization': 'Basic {}'.format(base64_encode)
}
And send post requests:
r = requests.post(AUTH_URL, data=ref_payload, headers=header)

How to return access token on login with oauth2 in drf?

I want to return the user access token for oauth2 as soon as the user logs in with a login api.
Till now I have created a login and register api and I am able to genereate access token via /o/token but I want it as a return value.
Here is my views.py :-
"""
POST auth/login/
"""
# This permission class will overide the global permission
# class setting
permission_classes = (AllowAny,)
serializer_class = UserSerializer
queryset = User.objects.all()
def post(self, request, *args, **kwargs):
username = request.data.get("username", "")
password = request.data.get("password", "")
user = authenticate(request, username=username, password=password)
if user is not None:
# login saves the user’s ID in the session,
# using Django’s session framework.
login(request, user)
return redirect('list-user')
return Response(status=status.HTTP_401_UNAUTHORIZED)
class RegisterUserView(generics.CreateAPIView):
"""
POST auth/register/
"""
permission_classes = (AllowAny,)
serializer_class = UserRegistrationSerializer
def post(self, request, *args, **kwargs):
username = request.data.get("username", "")
password = request.data.get("password", "")
email = request.data.get("email", "")
if not username and not password and not email:
return Response(
data={
"message": "username, password and email is required to register a user"
},
status=status.HTTP_400_BAD_REQUEST
)
new_user = User.objects.create_user(
username=username, password=password, email=email
)
return Response(status=status.HTTP_201_CREATED)
and here is my serializers.py
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'password']
class UserRegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email','password']
urls.py
path('admin/', admin.site.urls),
path('', include('users.urls')),
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]
urls.py for users app
urlpatterns = [
path('users/', views.UserListView.as_view(), name='list-user'),
path('auth/login/', views.LoginView.as_view(), name="auth-login"),
path('auth/register/', views.RegisterUserView.as_view(), name="auth-register")
]
How can I implement it?
You will have to get token from oauth2_provider view for auth token generation and and then you can modify response according to your requirement.
from oauth2_provider.views.base import TokenView
class CustomAuthView(generics.CreateAPIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
def post(self, request, *args, **kwargs):
oauth_response = TokenView.as_view(request, *args, **kwargs)
if oauth_response.status == 200:
data = oauth_response.data
# update data according to your requirement
return response.Response(data)
else:
return oauth_response
If you want to just change url for auth view, then you can do it by adding a new url which will point towards TokenView like this
from oauth2_provider.views.base import TokenView
path('auth/login/', TokenView.as_view(), name="auth-login"),
You will have to provide following parameters to your api
grant_type
username
password
client_id
client_secret

Apple SSO: Why I am getting a 'unsupported_grant_type' error?

Problem
When i try to validate the code returned from the Apple SSO client flow, I keep getting a unsupported_grant_type 400 error.
The docs say that an unsupported_grant_type will be returned when The authenticated client is not authorized to use the grant type. I've enabled Apple SSO on the App Id, the Service Id, and have even verified my support email domains. What am I missing? Is there some other approval step I need to complete to get authorized?
I've tried removing params from my verification request, but still get the same error code.
Details
The SSO redirect gives me a form-encoded POST body that looks something like this: {"state"=>"x", "code"=>"y", "id_token"=>"z"}.
I then attempt to validate the token by calling validate_auth_token.
def validate_auth_token(token, is_refresh = false)
uri = URI.parse('https://appleid.apple.com/auth/token')
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
headers = { 'Content-Type': 'text/json' }
request = Net::HTTP::Post.new(uri.path, headers)
request_body = {
client_id: #client_id,
client_secret: retreive_client_secret
}
if is_refresh
request_body[:grant_type] = 'refresh_token'
request_body[:refresh_token] = token
else
request_body[:grant_type] = 'authorization_code'
request_body[:code] = token
request_body[:redirect_uri] = "https://#{Rails.application.secrets.backend_host_port}/apple"
end
request.body = request_body.to_json
response = https.request(request)
p JSON.parse response.body
end
def retreive_client_secret
cert = retreive_secret_cert
ecdsa_key = OpenSSL::PKey::EC.new cert
algorithm = 'ES256'
headers = {
'alg': algorithm,
'kid': #key_id
}
claims = {
'iss': #team_id,
'iat': Time.now.to_i,
'exp': Time.now.to_i + 5.months.to_i,
'aud': 'https://appleid.apple.com',
'sub': #client_id
}
token = JWT.encode claims, ecdsa_key, algorithm, headers
token
end
Where #client_id is the "Service ID" I submitted in the initial SSO request, #key_id is the id of the private key downloaded from the apple key dashboard, and #team_id is our apple team id. retrieve_secret_cert simply gets the cert file body used to generate the client secret.
Given all this, I would expect a TokenResponse, but keep getting the same error {"error"=>"unsupported_grant_type"} with no additional explanation.
The token validation request needs to be form encoded, not json encoded. Also, the request wasn't validating correctly when I included an alg header in the JWT, but worked after I removed it.
Here's the updated code:
def validate_auth_token(token, is_refresh = false)
uri = URI.parse('https://appleid.apple.com/auth/token')
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
request_body = {
client_id: #client_id,
client_secret: retreive_client_secret
}
if is_refresh
request_body[:grant_type] = 'refresh_token'
request_body[:refresh_token] = token
else
request_body[:grant_type] = 'authorization_code'
request_body[:code] = token
request_body[:redirect_uri] = "https://#{Rails.application.secrets.backend_host_port}/auth"
end
request = Net::HTTP::Post.new(uri.path)
request.set_form_data(request_body)
response = https.request(request)
JSON.parse response.body
end
def retreive_client_secret
cert = retreive_secret_cert
ecdsa_key = OpenSSL::PKey::EC.new cert
algorithm = 'ES256'
headers = {
'kid': #key_id
}
claims = {
'iss': #team_id,
'iat': Time.now.to_i,
'exp': Time.now.to_i + 5.minutes.to_i,
'aud': 'https://appleid.apple.com',
'sub': #client_id
}
token = JWT.encode claims, ecdsa_key, algorithm, headers
token
end
Thank you sudhakar19 for pointing out the encoding error.

Resources