Oauth2 Flutter Authorization Parameters - flutter

I have a (possibly?) niche question about Oauth2 in Dart and Flutter (I'm using the standard Oauth2 package: https://pub.dev/packages/oauth2). Specifically, I need some values that are passed back as part of the Authorization flow, but they are not the standard values, so it's not returned as part of the client.
Specifically, as part of my flow, I run:
final returnValue = await authClient.authenticate(
authorizationUrl: authorizationUrl,
redirectUri: redirectUri!,
);
No problem there. Then I run:
client = await grant
.handleAuthorizationResponse(Uri.parse(returnValue).queryParameters);
Again, works properly, returns a Client that I can use. However, following this function down, handleAuthorizationResponse calls _handleAuthorizationResponse. The function _handleAuthorizationResponse ends with:
var response =
await _httpClient!.post(tokenEndpoint, headers: headers, body: body);
// print(response.headers);
// print(response.body);
var credentials = handleAccessTokenResponse(
response, tokenEndpoint, startTime, _scopes, _delimiter,
getParameters: _getParameters);
return Client(credentials,
identifier: identifier,
secret: secret,
basicAuth: _basicAuth,
httpClient: _httpClient,
onCredentialsRefreshed: _onCredentialsRefreshed);
This is where my issue is. That response has some fields in the body that I need. They're not standard, so they're not passed back as part of the Client. Without rewriting my own versions of these functions, is there another way I can get access to these values?
In answer to a question, this uses a SMART on FHIR launch (it's a launch framework that's basically just an oauth2 wrapper) - it's standard in healthcare. The return json from the accessToken would look something like this:
{
"need_patient_banner": true,
"smart_style_url": "https://smart.argo.run/smart-style.json",
"patient": "87a339d0-8cae-418e-89c7-8651e6aab3c6",
"token_type": "Bearer",
"scope": "launch/patient patient/Observation.rs patient/Patient.rs",
"expires_in": 3600,
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuZWVkX3BhdGllbnRfYmFubmVyIjp0cnVlLCJzbWFydF9zdHlsZV91cmwiOiJodHRwczovL3NtYXJ0LmFyZ28ucnVuLy9zbWFydC1zdHlsZS5qc29uIiwicGF0aWVudCI6Ijg3YTMzOWQwLThjYWUtNDE4ZS04OWM3LTg2NTFlNmFhYjNjNiIsInRva2VuX3R5cGUiOiJiZWFyZXIiLCJzY29wZSI6ImxhdW5jaC9wYXRpZW50IHBhdGllbnQvT2JzZXJ2YXRpb24ucnMgcGF0aWVudC9QYXRpZW50LnJzIiwiY2xpZW50X2lkIjoiZGVtb19hcHBfd2hhdGV2ZXIiLCJleHBpcmVzX2luIjozNjAwLCJpYXQiOjE2MzM1MzIwMTQsImV4cCI6MTYzMzUzNTYxNH0.PzNw23IZGtBfgpBtbIczthV2hGwanG_eyvthVS8mrG4",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7Im5lZWRfcGF0aWVudF9iYW5uZXIiOnRydWUsInNtYXJ0X3N0eWxlX3VybCI6Imh0dHBzOi8vc21hcnQuYXJnby5ydW4vL3NtYXJ0LXN0eWxlLmpzb24iLCJwYXRpZW50IjoiODdhMzM5ZDAtOGNhZS00MThlLTg5YzctODY1MWU2YWFiM2M2In0sImNsaWVudF9pZCI6ImRlbW9fYXBwX3doYXRldmVyIiwic2NvcGUiOiJsYXVuY2gvcGF0aWVudCBwYXRpZW50L09ic2VydmF0aW9uLnJzIHBhdGllbnQvUGF0aWVudC5ycyBvZmZsaW5lX2FjY2VzcyIsImlhdCI6MTYzMzUzMzg1OSwiZXhwIjoxNjY1MDY5ODU5fQ.Q41QwZCEQlZ16M7YwvYuVbUP03mRFJoqRxL8SS8_ImM"
}
So it has the typical values of an accessToken (expires_in, token_type, etc), but it also has things like 'patient'. Those are the values that I need.
In my example, the final Credentials (client.credentials looks like this):
{
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ1cm46b2lkOmZoaXIiLCJjbGllbnRfaWQiOiIzZmE1Y2FmOS04YTk4LTQ4MjgtOTJkZS01OWU2NmJjYjIwNjQiLCJlcGljLmVjaSI6InVybjplcGljOk9wZW4uRXBpYy1jdXJyZW50IiwiZXBpYy5tZXRhZGF0YSI6IjVYWnFRU0lrSk9PNi1XRkhpVXBiMlg2ak5MQzJ1aDFQdWtaSHVWSHkzcTdJMTBBT1BfYXR5V0tEb19LMlRFRF9ic21TNk9UTmZiVGtISnY1dnFvM1RtTVduRlk2RDBPSlE2WkRhd1NJWkk4WDh0Xy1XT1pYWEs1WjFrcTNBNm9mIiwiZXBpYy50b2tlbnR5cGUiOiJhY2Nlc3MiLCJleHAiOjE2NTQ2NDEzMDIsImlhdCI6MTY1NDYzNzcwMiwiaXNzIjoidXJuOm9pZDpmaGlyIiwianRpIjoiMDg1ZTdhNTYtY2I0OS00Zjg3LWFiYmEtZDg0M2ZmODI2YmQ2IiwibmJmIjoxNjU0NjM3NzAyLCJzdWIiOiJlYjRHaWE3RnlpanRQbVhrcnRqUnBQdzMifQ.rAKweImVE86oF3ciZDGhDysrYY9-XV6fBbyzqkQiJxHg-V-zImW414m3X5wKcP9B0J1MMdJCwg5DTpcbd0iU-N3SXRVXxBO2BqTcMAGLr-jlepnqBfu1Esg0nAI9jVasSWhz6tXFcLWOoCocg1hLcMfY875xnszwztJiJieDhumKZSStcsQM4KR9lUQZdJ3-U6IXV7wn3kaD4GQBSPZ0OkUe2d8zdCpjcbGCO-wWNdfe_sQDd7k7MbBJ1ryFRtd45GSzhKFa3Cch8kWTo3bGPlzzFuvhX_kbX1WtqTXaeB2G-o49lT4RJldnZi62L51VtS69_M15EsQtmMRHg6WMEA",
"refreshToken": null,
"idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6InRvVzlqTVVTTi81L0wzaXdhUUdkVG1ORHVodnAvSmNBWlZIL2NPSjZPckU9IiwidHlwIjoiSldUIn0.eyJhdWQiOiIzZmE1Y2FmOS04YTk4LTQ4MjgtOTJkZS01OWU2NmJjYjIwNjQiLCJleHAiOjE2NTQ2MzgwMDIsImZoaXJVc2VyIjoiaHR0cHM6Ly9maGlyLmVwaWMuY29tL2ludGVyY29ubmVjdC1maGlyLW9hdXRoL2FwaS9GSElSL1I0L1BhdGllbnQvZXJYdUZZVWZ1Y0JaYXJ5VmtzWUVjTWczIiwiaWF0IjoxNjU0NjM3NzAyLCJpc3MiOiJodHRwczovL2ZoaXIuZXBpYy5jb20vaW50ZXJjb25uZWN0LWZoaXItb2F1dGgvb2F1dGgyIiwic3ViIjoiZXJYdUZZVWZ1Y0JaYXJ5VmtzWUVjTWczIn0.hh33_q4f3tnioB7Iq6jY07-m5i_OsaqUt_kg_ZnPMGPKK8AnYVk3Tps2XTdUzUIHizFRWlGmAT_E0F283LBmVPTrbtD_X6EwqmUbTBrWj94RyvE-k3ofoEo-CwbSJZXu8MrQTb3DzpRKTGo7D1sI5E4UqnKQhPWFmhwCjMXpbdRy6bddb14fdWZzjS_Ffq4OsNRIalnePR8z1zNtSy14_RCiSh8o2elkj3p1AOmSXeD9-nZ91Z646lt4C5oP9gwN7OhmBovQRuDYaql1tz1aHOhilIsBZc1jMxEZJ65cekmFy6HZ4rME23xg-EQHu7XhKWOpOjovbMPwapSlC-eUcA",
"tokenEndpoint": "https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token",
"scopes": [
"patient/Patient.read",
"patient/Questionnaire.read",
"patient/QuestionnaireResponse.Read",
"fhirUser",
"launch/patient",
"openid",
"profile"
],
"expiration": 1654641291566
}
I've tried decoding the accessToken and the idToken and neither of them have fields like "patient". Howevever, if after I get the response and print the body, it looks like this:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ1cm46b2lkOmZoaXIiLCJjbGllbnRfaWQiOiIzZmE1Y2FmOS04YTk4LTQ4MjgtOTJkZS01OWU2NmJjYjIwNjQiLCJlcGljLmVjaSI6InVybjplcGljOk9wZW4uRXBpYy1jdXJyZW50IiwiZXBpYy5tZXRhZGF0YSI6IkJJR1dLcDZPbU1ZcmJkT2YzMkFvX0djRmtiUG1RbEtZcHdEN1d4S0VLNEJXQTZvSUxXVUdRd05SUWdtMUdsd00xTzE0YUVZNi1GTmd6Si1jMGRiQ0puODRIUEU4SDEzNmp6UzNCZGFDUVBMSXFyYXpZenF3Uldyd1MwX3NWU1k5IiwiZXBpYy50b2tlbnR5cGUiOiJhY2Nlc3MiLCJleHAiOjE2NTQ2NDc2MDgsImlhdCI6MTY1NDY0NDAwOCwiaXNzIjoidXJuOm9pZDpmaGlyIiwianRpIjoiMTA1OTFmOTMtYjUxMi00MmNlLTk1YjgtYjY2NTEwODM0MzNkIiwibmJmIjoxNjU0NjQ0MDA4LCJzdWIiOiJlYjRHaWE3RnlpanRQbVhrcnRqUnBQdzMifQ.AJOd9g8YAJp91n0qY3Hg9F2sNpo26VMYKpNKR5y7CIV8zrADh2whv2WRm8gi-cIeS6XUR6UzXyzXVJ9Ips5FgFdIZ4yQI_HXxH9r8aeF6VS6jT-ZQygtzWnVYeyJvu-1b3YpbgdCd3KTrnWLwhU3vqUmil2L8gJzWG473ihXDz-7ezsJBBl9R-c5Ap_L6WF6Ox8lHH6mgwbZHeKr0U0aYne-QLM7mylsPC5BC_WlUOwMnEJ73DKjF2E0X6wMCP7jMieJxhpkTIDRwKQbuGwLjtneS-Efu69NHGsxSP_m3aN652rdh9-b5WyIsT-DqjPHxHTtbxGQI-WthHOhnLaDkQ",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "patient/Patient.read patient/Questionnaire.read patient/QuestionnaireResponse.Read fhirUser launch/patient openid profile",
"__epic.dstu2.patient": "TnOZ.elPXC6zcBNFMcFA7A5KZbYxo2.4T-LylRk4GoW4B",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InRvVzlqTVVTTi81L0wzaXdhUUdkVG1ORHVodnAvSmNBWlZIL2NPSjZPckU9IiwidHlwIjoiSldUIn0.eyJhdWQiOiIzZmE1Y2FmOS04YTk4LTQ4MjgtOTJkZS01OWU2NmJjYjIwNjQiLCJleHAiOjE2NTQ2NDQzMDgsImZoaXJVc2VyIjoiaHR0cHM6Ly9maGlyLmVwaWMuY29tL2ludGVyY29ubmVjdC1maGlyLW9hdXRoL2FwaS9GSElSL1I0L1BhdGllbnQvZXJYdUZZVWZ1Y0JaYXJ5VmtzWUVjTWczIiwiaWF0IjoxNjU0NjQ0MDA4LCJpc3MiOiJodHRwczovL2ZoaXIuZXBpYy5jb20vaW50ZXJjb25uZWN0LWZoaXItb2F1dGgvb2F1dGgyIiwic3ViIjoiZXJYdUZZVWZ1Y0JaYXJ5VmtzWUVjTWczIn0.wxlvguGhAZdWJiSpX1-jzANXk0hFhLeIPFS5BlnIJLLZg8ibvpzLutQr2Z7Rg_d07_amI4gGbNigso9gvPbN5e1jjDGZkU2QYUbcLZbwkTcxXfVWOsyAADOZZrqx0J1yrGIeA4V4EfqQ4xBym_e8CeEjGP9L4ouRBKK6AHR5N5Mmdo_I4_RoPr-mCR2e2Q_of7tYFuhcl8mHaT6brbn-ZoEuAMgAQztF-7SBpDSvRB1C4HzV6mk-Hql0jNhZ0WefZe_ve0gB3exdWDjCLClpRRjt_MRaFTYGPqiZuyJF-dEFEqNar1Y5BRjQmUdJbDWj8ecfWaldigXNVAvNthbs4g",
"patient": "erXuFYUfucBZaryVksYEcMg3"
}
So you can see why I need access to that information. Any idea, apart from extending the class and overriding the functions I could get access to it?

access_token contains all the information that is in the response.
{
"need_patient_banner": true,
"smart_style_url": "https://smart.argo.run//smart-style.json",
"patient": "87a339d0-8cae-418e-89c7-8651e6aab3c6",
"token_type": "bearer",
"scope": "launch/patient patient/Observation.rs patient/Patient.rs",
"client_id": "demo_app_whatever",
"expires_in": 3600,
"iat": 1633532014,
"exp": 1633535614
}
Access token is not meant to be read by the client, but ID token is for that. Defining the right scopes in authentication request will also return ID token.
Client has a credentials property, which holds all the tokens.
id_token's sub field seems to match patient. There is also fhirUser user endpoint, which probably gives you more information about the patient.
{
"aud": "3fa5caf9-8a98-4828-92de-59e66bcb2064",
"exp": 1654644308,
"fhirUser": "https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4/Patient/erXuFYUfucBZaryVksYEcMg3",
"iat": 1654644008,
"iss": "https://fhir.epic.com/interconnect-fhir-oauth/oauth2",
"sub": "erXuFYUfucBZaryVksYEcMg3"
}

So for anyone running into the same problem, so far I've found the answer is that you can't do it from the client side. This is an issue with the response returned from the server. If you don't have access to the server, you're SOL and will have to use a workaround like creating a proxy to send and accept all of your queries.
Specifically for my problem above, it was that the server did not include the following header
Access-Control-Expose-Headers:Location
Here are some other places to read about CORS that I found at least somewhat helpful:
Set cookies for cross origin requests
How to solve flutter web api cors error only with dart code?
https://github.com/flutterchina/dio/issues/1027
https://appvesto.medium.com/how-to-add-cors-to-the-dart-server-9d55a2835397

Related

Cloud Run Service requests always fail with 401 when using JWT returned for another service. The same request works with JWT returned for end-user

I have an app consisting of multiple Cloud Run Services. Each service is open only to a specific set of other services.
Let's say I have 2 services with URLs https://api-service-123.run.app and https://data-provider-service-123.run.app. The first service also has a custom URL https://api.my.domain.
api-service needs access to data-provider-service. So, following the documentation, I created two per-service user-managed service-accounts api-service-account#domain and data-provider-service-account#domain. I added api-service-account#domain into data-provider-service with Cloud Run Invoker role.
Now, I have trouble accessing the account with ID Token returned from Google. I tried several ways. Firstly, I utilized slightly adjusted code from the docks
export const getToken = async (url: string, targetAUD?: string) => {
const auth = new GoogleAuth();
const request = async () => {
if (!targetAUD) {
targetAUD = new URL(url).origin;
}
console.info(`request ${url} with target audience ${targetAUD}`);
const client = await auth.getIdTokenClient(targetAUD);
const res = await client.request({url});
console.info(res.data);
return res.data;
}
try {
return await request();
} catch (err) {
console.error(err);
}
}
When the function above is called as getToken('http://data-provider-service-123.run.app');, the request fails with 401 Unauthorized.
I also tried to call it as getToken('https://data-provider-service-123.run.app'); and got a response 403 Forbidden. The following entry is added to data-provider-service logs for each such request
{
httpRequest: {9}insertId: "60fcb7e0000c12346fe37579"
logName: "projects/my-project/logs/run.googleapis.com%2Frequests"
receiveTimestamp: "2021-07-25T01:01:20.806589957Z"
resource: {2}
severity: "WARNING"
textPayload: "The request was not authorized to invoke this service. Read more at
https://cloud.google.com/run/docs/securing/authenticating"
timestamp: "2021-07-25T01:01:20.800918Z"
trace: "projects/my-project/traces/25b3c13890aad01234829711d4e9f25"
}
I also tried to obtain and use JWT from compute metadata server (also a way advertised in the docs) by running curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=http://data-provider-servive-123.run.app" \ -H "Metadata-Flavor: Google".
When I execute this code in Cloud Shell, I get JWT that looks like this
{
"iss": "accounts.google.com",
"azp": "123456789-9r9s1c4alg36erliucho9t52n12n6abc.apps.googleusercontent.com",
"aud": "123456789-9r9s1c4alg36erliucho9t52n12n6abc.apps.googleusercontent.com",
"sub": "106907196154567890477",
"email": "my_email#gmail.com",
"email_verified": true,
"at_hash": "dQpctkQE2Sy1as1qv_w",
"iat": 1627173901,
"exp": 1627177501,
"jti": "448d660269d4c7816ae3zd45wrt89sb9f166452dce"
}
Then I make a request using postman to https://data-provider-service/get-data with a header Authorization: "Bearer <the_jwt_from_above>" and everything works fine
However, if I make the same request from my api-service, the JWT returned is
{
"aud": "http://data-provider-service-123.run.app",
"azp": "111866132465987679716",
"email": "api-service-account#domain",
"email_verified": true,
"exp": 1627179383,
"iat": 1627175783,
"iss": "https://accounts.google.com",
"sub": "111866132465987679716"
}
If I make the same request with the postman and place this JWT into the Authorization header, the 401 Unauthorized is returned.
I have spent this week trying to solve this issue. I triple-checked the permissions, redeployed the services several times, but nothing helps.
I changed http to https in the URL of the target service when asking computing metadata server, and looks like it solved the problem. Requesting token like this with google-auth-library still fails with 403 error.
I'll update that answer if the solution is sustainable.

JWT token miss the claim in Nuxt Auth module with Auth0

In my Nuxt app after successful log in I have claim from Auth0. Devtools console shows it:
$vm0.$auth.$state.user['https://hasura.io/jwt/claims']
{x-hasura-default-role: "user", x-hasura-allowed-roles: Array(1), x-hasura-user-id: "auth0|5e989a*******"}
But my token doesn't have this claim, so my hasura requests fails. I check token right after claim in console:
$vm0.$auth.getToken('auth0')
"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik1ESTNNa0l5...
And I decode it using https://jwt.io/ and there is no claim:
{
"iss": "https://*******.eu.auth0.com/",
"sub": "auth0|5e989a*******",
"aud": [
"https://*******.eu.auth0.com/api/v2/",
"https://*******.eu.auth0.com/userinfo"
],
"iat": 1587059738,
"exp": 1587066938,
"azp": "MxJB0Y9dxvdGZnTAb2a4Y0YOHArvbkWt",
"scope": "openid profile email"
}
Here is my claim script in Auth0 dashboard:
function (user, context, callback) {
const namespace = "https://hasura.io/jwt/claims";
context.idToken[namespace] =
{
'x-hasura-default-role': 'user',
// do some custom logic to decide allowed roles
'x-hasura-allowed-roles': ['user'],
'x-hasura-user-id': user.user_id
};
callback(null, user, context);
}
I fixed this issue by changing claim script in Auth0 dashboard.
It should be context.accessToken[namespace] = {...} instead of context.idToken[namespace] = {...} and now I have that claim data inside my token.

Amadeus flight API authorization

I am trying to use one of the Amadeus flight API (flight lowest-fare search) and I think I am having issues with the authorization. The first part of the authorization works fine where I have to input the grant type, API key, and API secret. This returns the access token needed to return the flight API. However, when printing the response body I keep getting null. Could someone help me with this? I am showing my API key and API secret but that's not an issue as I can create a new one. Here's the code:
To first provide context, here's how an Amadeus API gets called after access token is retrieved from the authorization server. This endpoint returns a flight's check-in links.
curl -X GET \
"https://test.api.amadeus.com/v2/reference-data/urls/checkin-links?airlineCode=1X" \
-H "Authorization: Bearer CpjU0sEenniHCgPDrndzOSWFk5mN"
I believe my issue might in my authorization header in the flight low-fare search endpoint. I concatenated the two variables in which the token_type which has a value of 'Bearer' and the access token. In the curl example, 'Bearer' is within the speech marks. In flutter, you cannot do that as 'Authorization is the only header. Below is my code in dart:
getFlights(fromCode, toCode) async {
// getting access token
var response = await http.post(
'https://test.api.amadeus.com/v1/security/oauth2/token',
body: {
"grant_type": "client_credentials",
"client_id": "cxuxBmIvbpbv0JKzKTsJjNqc595uJVYb",
"client_secret": "buhj4SDGVrG1AzzV",
},
);
if (response.statusCode == 200) {
try {
print(response.body);
var code = jsonDecode(response.body);
if (code != null) {
var tokenType = code['token_type'];
print(tokenType);
print(code['access_token']);
var token = code['access_token'];
var bearerToken = '$tokenType ' + '$token';
print(bearerToken);
// flight low-fare search endpoint
var flightApi = await http.get(
'https://test.api.amadeus.com/v1/shopping/flight-offers?origin=LHR&destination=CDG&departureDate=2020-03-19&max=2',
headers: {
"Authorization": bearerToken,
});
var flight = json.decode(response.body);
print(flight['data']);
}
} catch (e) {
print(e.toString());
}
}
}
This the return from the authorization server which provides the access token:
{
"type": "amadeusOAuth2Token",
"username": "I REMOVED THIS",
"application_name": "I REMOVED THIS",
"client_id": "cxuxBmIvbpbv0JKzKTsJjNqc595uJVYb",
"token_type": "Bearer",
"access_token": "z8rVGOAuGaXGNUMIcVPYW76ki5Dl",
"expires_in": 1799,
"state": "approved",
"scope": ""
}
This is the return for the flight low-fare search endpoint
flutter: null

Jhipster + REST client + authentication

I need to understand how to authenticate a REST client (could be Paw, could be an android app, an iOs app using AFNetworking with jHipster and I think, more in general, with spring-boot of which I am no expert).
While I am able to obtain a token when logged in a browser, and subsequently use this token in the following requests, I do not understand how I can authenticate in the first place using RESTful best practices.
For example, in Paw.app, I can pass a Basic authentication, or Oauth2, but I don't understand how to get the session token simply authenticating as I do on a web browser.
Similarly, in AFNetworking I am able to pass basic authentication, e.g.
NSString*auth=[NSString stringWithFormat:#"%#:%#", #"admin", #"admin"];
NSString *authValue = [NSString stringWithFormat:#"Basic %#", [auth base64EncodedString]];
[manager.requestSerializer setValue:authValue forHTTPHeaderField:#"Authorization"];
But I struggle to understand how to authenticate with the session security which is bundled in jHipster/spring boot.
First of all, do not use HTTP session authentication for mobile apps.
On the other hand, Oauth2 or JWT works fine with mobile apps. The basic idea behind them is to get a token from Jhipster to mobile the token has an expiry time. In that time you can use the token to access any REST API of Jhipster to access data.
Below I am showing the code snippet of how I was using the Jhipster rest API in my angularjs based ionic app. I hope it gives the idea of what you need to do.
uncomment cors in application.yml inside jhipster
cors: #By default CORS are not enabled. Uncomment to enable.
allowed-origins: "*"
allowed-methods: GET, PUT, POST, DELETE, OPTIONS
allowed-headers: "*"
exposed-headers:
allow-credentials: true
max-age: 1800
To access REST API with Oauth2 authentication in ionic you must first get the token in the ionic app by
$http({
method: "post",
url: "http://192.168.0.4:8085/[Your app name]/oauth/token",
data: "username=admin&password=admin&grant_type=password&scope=read write&client_secret=my-secret-token-to-change-in-production&client_id=auth2Sconnectapp",
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'Authorization': 'Basic ' + 'YXV0aDJTY29ubmVjdGFwcDpteS1zZWNyZXQtdG9rZW4tdG8tY2hhbmdlLWluLXByb2R1Y3Rpb24='
}
})
.success(function(data) {
alert("success: " + data);
})
.error(function(data, status) {
alert("ERROR: " + data);
});
Here "YXV0aDJTY29ubmVjdGFwcDpteS1zZWNyZXQtdG9rZW4tdG8tY2hhbmdlLWluLXByb2R1Y3Rpb24=" is equal to (clientId + ":" + clientSecret)--all base64-encoded
The above $http if successful will give you this JSON which contains token and it's expiry time
{
"access_token": "2ce14f67-e91b-411e-89fa-8169e11a1c04",
"token_type": "bearer",
"refresh_token": "37baee3c-f4fe-4340-8997-8d7849821d00",
"expires_in": 525,
"scope": "read write"
}
Take notice of "access_token" and "token_type" if you want to access any API this is what you have to use. We send the token with API to access data until the token expires then we either refresh it or access for a new one.
For example
$http({
method: "get",
url: "http://192.168.0.4:8085/auth-2-sconnect/api/countries",
withCredentials: true,
headers: {
'Authorization':' [token_type] + [space] + [access_token] '
}
})
.success(function(data) {
alert("success: " + data);
})
.error(function(data, status) {
alert("ERROR: " + data);
});
Here a summarisation of how I implemented the solution. It’s real swift code, but please take it as pseudocode, as it might be incorrect.
make a call to whatever method you need to call, passing in such method a callback (block, or equivalent) for the success and one for the failure
func action(
URLString:String,
method:Method,
encoding:Encoding = .JSON,
parameters:[String : AnyObject]?,
success:(statusCode:Int, responseObject:AnyObject)->Void,
failure:(statusCode:Int, error:NSError)->Void
)
Inside the method es. /events you handle a particular case of failure, which is when the status code is 401.
if(r!.statusCode==ResponseCodes.HTTP_UNAUTHORIZED.rawValue){
loginAndAction(URLString, method: method, encoding: encoding, parameters: parameters, success: success, failure: failure)
}else{
failure(statusCode: response.response!.statusCode, error:response.result.error!)
}
In this particular case, instead of returning back the result and calling the failure callback, you call a login() method which, after the necessary parameters, accept the original success() callback
func loginAndAction(
URLString:String,
method:Method,
encoding: Encoding,
parameters:[String:AnyObject]?,
success:(statusCode:Int, responseObject:AnyObject)->Void,
failure:(statusCode:Int, error:NSError)->Void
)->Void
if the authentication succeeds
var d:[String:AnyObject] = response.result.value as! [String:AnyObject]
self.authToken = d["access_token"] as! String
action(URLString, method: method,encoding:encoding, parameters: parameters, success: success, failure: failure)
at this point the method action could use a proper working token.
This should happen only once a day (based on the token expiration), and it is a mechanism appliable to the oauth2 refresh_token call.

OAuth 2 Endpoint: Additional Fields?

Is is acceptable to include additional fields in an OAuth token endpoint response?
For example, I am returning UserId, Username and CompanyId below:
{
"access_token": "pHd4Wz1EF...",
"token_type": "bearer",
"expires_in": 86399,
"UserId": "7e7fbc39-8abd-41e1-b165-9d18b635b7a7",
"Username": "user#somewhere.com",
"CompanyId": "874f380a-76eb-49b1-81b5-a42100f7e4d0",
}
This essentially means that additional requests won't be necessary in order to get this information.
You can do that - but a more standardized solution would be to use "OpenID Connect" for that purpose (see http://openid.net/connect/).