-
-
Notifications
You must be signed in to change notification settings - Fork 218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ABDM changes for consuming APIs in the mobile app. #32194
Changes from 7 commits
17cbc8f
0ef7fa6
45bc345
d5dc397
5816f0b
3a3d1d9
0096518
707b685
d2c22e6
281abd1
f0480d4
e4f4cce
75cee53
a2bc2f4
8332e93
00c2bc0
7ac580b
3dce722
a3b9108
2a676da
a41d145
126cea1
56fc7fc
580e4ba
67f9165
4709df6
cb1869c
3be941b
e33ba12
804bebe
2121e22
fb51e67
d8bc231
b77ba0e
2b43e71
777e777
ebd9fd5
587a622
5ea0f1a
00e0ccd
59a0658
8668562
be165a7
299f292
5954509
f3f3c29
6be3231
0c6ce4b
6e0e44c
fd5307e
2b3b509
3b6327d
ba82c67
23d321b
424af6d
f7e8afd
09c8c5a
496bd0b
d8e49bf
b96be7f
f759674
5a48dce
d9cce87
13240dc
53c2a7b
f7917ab
56b14a5
847ec50
24a16d5
5a620bd
5423bc1
04f7876
276fd41
e1aeba7
c8e5f47
6a5a30a
1e37222
fbc3d87
01bdea4
19c30b9
b434fc6
be99017
d3c670b
0f142f0
1d35b7d
2f304e2
9c0e723
b0bdda5
95bb118
a6a26a4
6c86188
e8a9846
bb0fd46
4b86cad
e51a2da
0bec349
0eed91a
643b2bb
da0ba4e
7119925
44755b4
eba1c36
a4c9792
bd20ea8
e4d47ce
5328d10
d6b199f
1d099f8
0bc1aad
8628a7e
94436e6
3761567
ee5a952
e604d66
4496d8c
2055e5f
6b16f49
60d5df1
69b5818
961def7
75fbce7
f4e73e8
54bf4c0
df54e23
63bd5db
716051d
93b588c
29bb536
04260fd
dbd9660
6172be0
05a506d
8486963
492daad
2ed59f4
850019b
0821b24
7e85b13
8d58e89
3547632
aed8e7e
ebed0b8
1943140
987ee41
4db0721
b9047da
a4eb5c9
6424929
b379963
d4eb4b2
7235e7f
6587471
d9a2787
a9671d9
7883b48
07b6743
11d5047
502d1e1
0071f2f
e78d971
cdc21fd
b157ed6
cab23cc
a238477
bdea408
b66f25d
d291ae3
32d2961
3e5e07e
9a7d94b
17c0050
88c05d5
4830ebe
f1ae3ec
6083729
d234d42
3f6b2fb
5edb2ac
48e79ca
3d8615b
31a846a
f518467
6b5d317
44b9aa2
1274cfd
8e7f4a2
52bd4d1
1094b43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
from rest_framework.authentication import TokenAuthentication | ||
from rest_framework.exceptions import AuthenticationFailed | ||
from custom.abdm.models import ABDMUser | ||
|
||
|
||
class ABDMUserAuthentication(TokenAuthentication): | ||
|
||
def authenticate(self, request): | ||
access_token = request.META.get('HTTP_AUTHORIZATION', "") | ||
token = access_token.split(" ")[-1] | ||
if not token: | ||
return None | ||
try: | ||
user = ABDMUser.objects.get(access_token=token) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you make this model comply with the rest framework key structure then you could just do this:
A rest framework token has a 'key' and 'user' fields (user is a FK to the Django user). I'm also unclear why you need a custom model for the token and can't either use the existing HQApiKey or else the DRF Token model. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intention is to restrict token's access to ABDM API only as it is accessed by a separate app. Will it possible to implement this limitation without a custom auth logic? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I remember that if those entries related to DRF's token were not added in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
except ABDMUser.DoesNotExist: | ||
raise AuthenticationFailed('Unauthorized') | ||
if not user.is_token_valid: | ||
raise AuthenticationFailed('Invalid Token') | ||
return (user, None) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Generated by Django 3.2.15 on 2022-09-26 05:56 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
replaces = [('abdm', '0001_initial'), ('abdm', '0002_auto_20220926_0547')] | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='ABDMUser', | ||
fields=[ | ||
('username', models.CharField(max_length=100, primary_key=True, serialize=False)), | ||
('access_token', models.CharField(blank=True, max_length=2000, null=True)), | ||
], | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import logging | ||
|
||
from custom.abdm.milestone_one.utils.request_util import get_response_http_post | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def generate_aadhar_otp(aadhaar_number): | ||
generate_aadhar_otp_url = "v1/registration/aadhaar/generateOtp" | ||
payload = {"aadhaar": str(aadhaar_number)} | ||
return get_response_http_post(generate_aadhar_otp_url, payload) | ||
|
||
|
||
def generate_mobile_otp(mobile_number, txnid): | ||
generate_mobile_otp_url = "v1/registration/aadhaar/generateMobileOTP" | ||
payload = {"mobile": str(mobile_number), "txnId": txnid} | ||
return get_response_http_post(generate_mobile_otp_url, payload) | ||
|
||
|
||
def verify_aadhar_otp(otp, txnid): | ||
verify_aadhaar_otp_url = "v1/registration/aadhaar/verifyOTP" | ||
payload = {"otp": str(otp), "txnId": txnid} | ||
return get_response_http_post(verify_aadhaar_otp_url, payload) | ||
|
||
|
||
def verify_mobile_otp(otp, txnid): | ||
verify_mobile_otp_url = "v1/registration/aadhaar/verifyMobileOTP" | ||
payload = {"otp": str(otp), "txnId": txnid} | ||
return get_response_http_post(verify_mobile_otp_url, payload) | ||
|
||
|
||
def create_health_id(txnid): | ||
create_health_id_url = "v1/registration/aadhaar/createHealthIdWithPreVerified" | ||
payload = { | ||
"email": "", | ||
"firstName": "", | ||
"healthId": "", | ||
"lastName": "", | ||
"middleName": "", | ||
"password": "", | ||
"profilePhoto": "", | ||
"txnId": txnid | ||
} | ||
return get_response_http_post(create_health_id_url, payload) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import requests | ||
|
||
from custom.abdm.milestone_one.utils.request_util import get_access_token, get_response_http_post, base_url | ||
|
||
|
||
def generate_auth_otp(health_id, auth_method): | ||
auth_otp_url = "v2/auth/init" | ||
payload = {"authMethod": auth_method, "healthid": health_id} | ||
return get_response_http_post(auth_otp_url, payload) | ||
|
||
|
||
def confirm_with_mobile_otp(otp, txn_id): | ||
confirm_with_mobile_otp_url = "v1/auth/confirmWithMobileOTP" | ||
payload = {"otp": otp, "txnId": txn_id} | ||
return get_response_http_post(confirm_with_mobile_otp_url, payload) | ||
|
||
|
||
def confirm_with_aadhaar_otp(otp, txn_id): | ||
confirm_with_aadhaar_otp_url = "v1/auth/confirmWithAadhaarOtp" | ||
payload = {"otp": otp, "txnId": txn_id} | ||
return get_response_http_post(confirm_with_aadhaar_otp_url, payload) | ||
|
||
|
||
def get_account_information(x_token): | ||
account_information_url = "v1/account/profile" | ||
headers = {"Content-Type": "application/json; charset=UTF-8"} | ||
token = get_access_token() | ||
headers.update({"Authorization": "Bearer {}".format(token), "X-Token": f"Bearer {x_token}"}) | ||
resp = requests.get(url=base_url + account_information_url, headers=headers) | ||
return resp.json() | ||
|
||
|
||
def search_by_health_id(health_id): | ||
search_by_health_id_url = "v1/search/searchByHealthId" | ||
payload = {"healthId": health_id} | ||
return get_response_http_post(search_by_health_id_url, payload) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import logging | ||
|
||
from functools import wraps | ||
from typing import List | ||
|
||
from custom.abdm.milestone_one.utils.response_handler import generate_invalid_req_response | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def required_request_params(params): | ||
""" | ||
Checks if the parameters provided in the decorator(a list of strings) are present in the DRF request. | ||
If not, raises 400 Bad Request error. | ||
""" | ||
|
||
def decorate(fn): | ||
if not (params and isinstance(params, List)): | ||
error_msg = "Request could not be validated as a valid input not provided. \ | ||
Required: List of parameters." | ||
logger.warning(error_msg) | ||
return generate_invalid_req_response(error_msg) | ||
|
||
@wraps(fn) | ||
def wrapped(request, *args, **kwargs): | ||
invalid_params = [] | ||
for param in params: | ||
if not request.data.get(param): | ||
snopoke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
invalid_params.append(param) | ||
if invalid_params: | ||
error_msg = f"Missing required parameter(s) in the request: {','.join(invalid_params)}" | ||
return generate_invalid_req_response(error_msg) | ||
return fn(request, *args, **kwargs) | ||
|
||
return wrapped | ||
|
||
return decorate |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import json | ||
from typing import Any, Dict | ||
import requests | ||
|
||
from django.conf import settings | ||
|
||
base_url = "https://healthidsbx.abdm.gov.in/api/" | ||
gateway_url = "https://dev.abdm.gov.in/gateway/v0.5/sessions" | ||
|
||
|
||
def get_access_token(): | ||
payload = {"clientId": settings.ABDM_CLIENT_ID, "clientSecret": settings.ABDM_CLIENT_SECRET} | ||
headers = {"Content-Type": "application/json; charset=UTF-8"} | ||
resp = requests.post(url=gateway_url, data=json.dumps(payload), headers=headers) | ||
if resp.status_code == 200: | ||
return resp.json().get("accessToken") | ||
|
||
|
||
def get_response_http_post(api_url, payload, additional_headers): | ||
req_url = base_url + api_url | ||
headers = {"Content-Type": "application/json"} | ||
token = get_access_token() | ||
headers.update({"Authorization": "Bearer {}".format(token)}) | ||
if additional_headers: | ||
headers.update(additional_headers) | ||
resp = requests.post(url=req_url, data=json.dumps(payload), headers=headers) | ||
resp.raise_for_status() | ||
return resp.json() | ||
snopoke marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from rest_framework.response import Response | ||
|
||
from rest_framework.status import HTTP_200_OK, HTTP_500_INTERNAL_SERVER_ERROR, HTTP_400_BAD_REQUEST | ||
|
||
success_response_keys = ["txnId", "healthIdNumber", "auth_methods", "token"] | ||
|
||
|
||
def get_response(response_data): | ||
if not response_data: | ||
return Response({"error": "No valid response found"}, status=HTTP_500_INTERNAL_SERVER_ERROR) | ||
if any([key in response_data for key in success_response_keys]): | ||
return _get_success_abdm_response(response_data) | ||
return _get_error_abdm_response(response_data) | ||
|
||
|
||
def generate_invalid_req_response(message): | ||
resp = { | ||
"code": "400", | ||
"message": "Unable to process the current request due to incorrect data entered.", | ||
"details": [{ | ||
"message": message, | ||
"attribute": None | ||
}] | ||
} | ||
return Response(resp, status=HTTP_400_BAD_REQUEST) | ||
|
||
|
||
def _get_success_abdm_response(response_data): | ||
return Response(response_data, status=HTTP_200_OK) | ||
|
||
|
||
def _get_error_abdm_response(response_data): | ||
if "code" in response_data: | ||
return _parse_response(response_data) | ||
return generate_invalid_req_response(response_data) | ||
|
||
|
||
def _parse_response(response_data): | ||
status_code = int(response_data.get("code").split("-")[-1]) | ||
return Response(response_data, status=status_code) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import logging | ||
|
||
from custom.abdm.models import ABDMUser | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def get_abdm_api_token(username): | ||
user, _ = ABDMUser.objects.get_or_create(username=username) | ||
if not user.access_token: | ||
user.generate_token() | ||
return user.access_token |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
from rest_framework.decorators import api_view, permission_classes, authentication_classes | ||
from rest_framework.permissions import IsAuthenticated | ||
from custom.abdm.auth import ABDMUserAuthentication | ||
|
||
from custom.abdm.milestone_one.utils import abha_creation_util as abdm_util | ||
from custom.abdm.milestone_one.utils.decorators import required_request_params | ||
from custom.abdm.milestone_one.utils.response_handler import get_response | ||
|
||
|
||
@api_view(["POST"]) | ||
@permission_classes((IsAuthenticated,)) | ||
@authentication_classes((ABDMUserAuthentication,)) | ||
@required_request_params(["aadhaar"]) | ||
def generate_aadhaar_otp(request): | ||
aadhaar_number = request.data.get("aadhaar") | ||
resp = abdm_util.generate_aadhar_otp(aadhaar_number) | ||
return get_response(resp) | ||
|
||
|
||
@api_view(["POST"]) | ||
@permission_classes((IsAuthenticated,)) | ||
@authentication_classes((ABDMUserAuthentication,)) | ||
@required_request_params(["txn_id", "mobile_number"]) | ||
def generate_mobile_otp(request): | ||
txn_id = request.data.get("txn_id") | ||
mobile_number = request.data.get("mobile_number") | ||
resp = abdm_util.generate_mobile_otp(mobile_number, txn_id) | ||
return get_response(resp) | ||
|
||
|
||
@api_view(["POST"]) | ||
@permission_classes((IsAuthenticated,)) | ||
@authentication_classes((ABDMUserAuthentication,)) | ||
@required_request_params(["txn_id", "otp"]) | ||
def verify_aadhaar_otp(request): | ||
txn_id = request.data.get("txn_id") | ||
otp = request.data.get("otp") | ||
resp = abdm_util.verify_aadhar_otp(otp, txn_id) | ||
return get_response(resp) | ||
|
||
|
||
@api_view(["POST"]) | ||
@permission_classes((IsAuthenticated,)) | ||
@authentication_classes((ABDMUserAuthentication,)) | ||
@required_request_params(["txn_id", "otp"]) | ||
def verify_mobile_otp(request): | ||
txn_id = request.data.get("txn_id") | ||
otp = request.data.get("otp") | ||
resp = abdm_util.verify_mobile_otp(otp, txn_id) | ||
if resp and "txnId" in resp: | ||
resp = abdm_util.create_health_id(txn_id) | ||
resp.pop("token") | ||
resp.pop("refreshToken") | ||
return get_response(resp) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice if this used a toggle so that it only added the ABDM data if necessary. Also, this seems like a good place to use an extension point:
In corehq/ex-submodules/casexml/apps/phone/xml.py
in custom/abdm
You can read more about them here: https://commcare-hq.readthedocs.io/extensions.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to check for a particular application, rather than a domain? For example, if some property is set within an application, then only the key will be returned.
Will check the extensions part.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That could be possible. You would need to change the
get_registration_element
to accept more arguments (potentially just pass in the wholeRestoreState
class). The app would berestore_state.params.app
(which could be None if it is not an app specific restore)