Calling Google Sheets API in Swift with GTMAppAuth - swift

I'm trying to read/write to Google Sheets in Swift on macOS.
I'm using the GAppAuth library which in turn makes use of GTMAppAuth.
I managed to get authorized and get back both the access token and the refresh token but I still get an HTTP status code of 403 when I try to make a call to one of the Google Sheets' endpoints.
In applicationDidFinishLaunching(_:) I appended the following authorization scope, as detailed in the documentation:
GAppAuth.shared.appendAuthorizationRealm("https://www.googleapis.com/auth/spreadsheets.readonly")
But still, I get 403. What am I doing wrong? Am I adding the scope properly?
This is the code where I make the API call:
#IBAction func getSpreadsheet(_ sender: NSButton) {
let fetcherService = GTMSessionFetcherService()
fetcherService.authorizer = authorization
let spreadsheetInfo = URL(string: "https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetID}")
let fetcher = GTMSessionFetcher(url: spreadsheetInfoEndpoint)
fetcher.beginFetch { (data, error) in
//Error code 403
}
}

I got it working by looking at the cURL request with the Google API Explorer. The access token parameter was missing:
https://sheets.googleapis.com/v4/spreadsheets/[SPREADSHEETID]/values/[RANGE]?access_token=[access_token]
Also thanks to Jacques-Guzel Heron, in the comment section, who let me know of the official Google Auth Library for Swift. I'll check it out.

Related

Error 403: Request had insufficient authentication scopes (reading from Google Sheets in iOS App)

I am working on an app to interface and read from a google sheet.
I followed the quick-start guide in this link.
I am making requests to the Google Sheets API using an Oauth 2.0 client ID, but it is giving me an error stating the request had insufficient scopes (Error 403), but I am unsure why since that is the scope that is stated in the Quick-start guide? Not sure if the source of the error is in my setup in the Google Console or in my code below? I set up an Oauth 2.0 Client ID within the Google Console and copied that into my app adding it to one of the URL types in Xcode. I am using the kGTLRAuthScopeSheetsSpreadsheetsReadonly scope. Below is portion of my code performing the request in Swift.
private let service = GTLRSheetsService()
private let scopes = [kGTLRAuthScopeSheetsSpreadsheets, kGTLRAuthScopeSheetsSpreadsheetsReadonly, kGTLRAuthScopeSheetsDrive]
service.authorizer = GIDSignIn.sharedInstance().currentUser!.authentication.fetcherAuthorizer()
let range = "Class!A2:H"
let query = GTLRSheetsQuery_SpreadsheetsValuesGet
.query(withSpreadsheetId: spreadSheetID, range: range)
service.executeQuery(query, delegate: self, didFinish: #selector(ImportStrengthPlansVC.displayResultWithTicket(ticket:finishedWithObject:error:)))

iOS Today Widget utilize Uber login token

I have an app with an Uber login that gives access to restricted API calls (info on the current ride). I'd like to share the login token with the associated Today Widget so it can make similar calls.
I'm already sharing data with a UserDefaults suite, and I'm using the UberRides SDK. In digging into the RidesClient object it seems to try to use the keychain for storing/sharing the login token, and I set up a shared keychain to try to take advantage of this, but no luck. Restricted API calls from the widget return as unauthorized. Any suggestions?
Here's some code from the widget (note the user already authenticated in the main app):
let rc = RidesClient()
rc.fetchCurrentRide { ride, response in
if ride == nil { print("NO CURRENT RIDE") }
print(response.response)
print(response.error?.title)
if let ride = ride {
// do something
} else {
self.ride = nil
}
}
This returns an unauthorized response. I traced into the RidesClient (which is an object in the UberRides SDK), and see the code where the token is "supposed" to come from the keychain, but it doesn't.
I also tried generating my own URL request in the widget, using the login token passed through shared UserDefaults. This followed the standard HTTP access approach, putting the token in the Authorization header. But I got the same unauthorized response.
Here's some more details on the SDK approach:
Main app uses the LoginButton in native mode:
let scopes: [RidesScope] = [.Profile, .Places, .Request, .AllTrips]
let loginManager = LoginManager(accessTokenIdentifier: Configuration.getDefaultAccessTokenIdentifier(), keychainAccessGroup: "com.MYCOMPANY.MYAPP.share", loginType: .native)
let loginButton = LoginButton(frame: loginFrame, scopes: scopes, loginManager: loginManager)
loginButton.presentingViewController = self
loginButton.delegate = self
view.addSubview(loginButton)
The login button does the right thing and authorizes in the Uber app. I can see the token returned in the delegate callback didCompleteLoginWithToken. However, I can then check for the token:
let token = TokenManager.fetchToken(Configuration.getDefaultAccessTokenIdentifier(), accessGroup: "com.MYCOMPANY.MYAPP.share")
print(token)
The token is "nil". I don't think the SDK is saving the token into the access group keychain.
When I use the default keychain (not the keychainAccessGroup), the login in the app works fine and I can get the login token back and make restricted calls to the API. However, that doesn't help the widget, which needs the token from the access group keychain.
Solved!! After many hours of debugging, and searching. What was not clear in ANY documentation is the keychainAccessGroup MUST include the AppIndentifierPrefix. That's the 10 character identifier associated with the App ID. So, instead of using "com.MYCOMPANY.MYAPP.share", it's "APPID.com.MYCOMPANY.MYAPP.share" for the keychainAccessGroup.

Authorization header required Fitbit Swift oauth

So the error I am getting is:HTTP Status 401: Unauthorized, Response: {"errors":[{"errorType":"invalid_request","message":"Authorization header required.
The code I have to authorize Fitbit is :
func authorizeFitbit(completion: (result: Bool) -> Void ){
oauthswift.accessTokenBasicAuthentification = true
let state: String = generateStateWithLength(20) as String
oauthswift.authorizeWithCallbackURL( NSURL(string: "xxxxxxxxx")!, scope: "xxxxxxxxxxxxx", state: state, success: {
credential, response, parameters in
self.getUserProfile(){
(result: Bool) in
completion(result: result)
}
}, failure: { error in
print(error.localizedDescription)
completion(result: false)
})
}
I am able to grab profile information but when I am trying to grab more information from fitbit by clicking on an IBAction button, I am required to use this authorize function again to grab the number of steps.
The question may need more details to be properly answered.
What OAuth grant type are you using?
What is the oauthswift class you are using? Your own or third party?
You are stating that you are able to grab profile information. Should that be read as you been able to get the whole way through for whatever grant type you are using but get problems when you want to execute a new request to the resource server?
Some other OAuth implementations require you to set the Authorization HTTP-header with the actual token type and the actual access_token when you send your requests to the resource server. The following is an example for Microsoft Azure AD using Authorization Code flow:
GET /v1.0/me/messages
Host: https://graph.microsoft.com
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSU...
Another possibility, if I compare to the "Authorization Code" grant type, the "invalid_request" error implies that you have not got pass the first stage in the request flow, that error you get before requesting the access token.
EDIT: Didn't see how old this question was. Well, I let the reply be here anyway, maybe it helps someone one day...

Azure App Service Facebook

I've migrated from Azure Mobile Service to an App Service but I'm having difficulty working out how best to implement extended Facebook auth.
In my old implementation I inherited from FacebookLoginProvider and fetched the token from the claims. I then added the CustomFacebookLoginProvider to my login providers. I then use the token to fetch more information about the user (their date of birth, friends and gender). With this information I created a user object and saved it to my DB.
Does anyone have any suggestions on how best to recreate this in App Service as I can't find any documentation.
As far as how to set up Facebook authentication, you can find documentation here (and it sounds like you've already figured out this much):
https://azure.microsoft.com/en-us/documentation/articles/app-service-mobile-how-to-configure-facebook-authentication/
Now that Facebook authentication is set up, you can refer to the following which shows how to obtain user information:
https://azure.microsoft.com/en-us/documentation/articles/app-service-mobile-dotnet-backend-how-to-use-server-sdk/#user-info
// Get the credentials for the logged-in user.
var credentials =
await this.User
.GetAppServiceIdentityAsync<FacebookCredentials>(this.Request);
if (credentials.Provider == "Facebook")
{
// Create a query string with the Facebook access token.
var fbRequestUrl = "https://graph.facebook.com/me?access_token="
+ credentials.AccessToken;
// Create an HttpClient request.
using (var client = new System.Net.Http.HttpClient())
{
// Request the current user info from Facebook.
using (var resp = await client.GetAsync(fbRequestUrl))
{
resp.EnsureSuccessStatusCode();
// Do something here with the Facebook user information.
var fbInfo = await resp.Content.ReadAsStringAsync();
}
}
}
Note that you must add a using statement for System.Security.Principal to make the GetAppServiceIdentityAsync extension method work.
For more information on which Facebook user properties you can query, see the Facebook documentation here: https://developers.facebook.com/docs/graph-api/reference/user. Note that you may need to specify which user properties you want as an additional fields query string parameter on your call to the Facebook graph.
The only change I had to make when switching from Mobile Service to Mobile App was to change the end of the callback URL in the developer portal to use /.auth/login/facebook/callback instead of /signin-facebook and it worked exactly the same way as before.
Note that this is for a Windows app with a .NET backend; you didn't specify what you're using so your mileage may vary.
I've been using the following approach to obtain the Facebook Access Token in the iOS app.
App Services includes the Facebook Access Token in the request header, refer to https://azure.microsoft.com/en-in/documentation/articles/app-service-api-authentication/.
To get to the access token, create a Custom API in the Azure Portal, e.g. facebookUserInfo, with the following code:
module.exports = {
"get": function (request, response, next) {
response.send(200, { facebookAccessToken: request.headers['x-ms-token-facebook-access-token'] });
}};
In the iOS app, use the following code to query the custom API:
let client = self.table!.client
if client.currentUser != nil {
client.invokeAPI("facebookUserInfo", body: nil, HTTPMethod: "GET", parameters: nil, headers: nil, completion: { (result, response, error) -> Void in
if let resultDict = result {
if let facebookAccessToken = resultDict["facebookAccessToken"]! {
print(facebookAccessToken)
}
}
}
}
By Using the Easy Auth feature of Azure App Services, I dont need to worry about authentication.
I have a blogpost on this. I have explained on how we can use the FB GraphApi's to query FB data. Here is the link: https://blogs.msdn.microsoft.com/kaushal/2017/06/08/using-easy-auth-to-query-facebook-information-via-graph-api/
I have the sample code deployed on Github. Here is the link: https://github.com/kaushalp/Facebook-GraphApi-with-EasyAuth

redirect uri causing issues when navigating to an oauth url

I am trying to navigate to the Microsoft Live oauth authentication page so that I can authorize my user and get a token for use by the app. When I use the following NSURL string, I am able to navigate to the site and authorize my app, retrieving the token.
let stringUrl = "https://login.live.com/oauth20_authorize.srf?client_id=\(clientId)&scope=\(scope)&response_type=code"
let url = NSURL(string: stringUrl)!
However, I want to redirect back to my app (using SFSafariViewController). In order to do so, I added a URL Scheme to my app, and passed that in as the redirect_uri in the URL.
let stringUrl = "https://login.live.com/oauth20_authorize.srf?client_id=\(clientId)&scope=\(scope)&response_type=code&redirect_uri=Lifestream://onedrive-oauth-callback"
let url = NSURL(string: stringUrl)!
However the Live login site gives me an error saying something went wrong and it couldn't continue. This error occurs before the oauth login page is displayed. It happens immediately upon navigating to the URL.
Am I creating my URL scheme incorrectly, or passing the scheme in to the redirect uri improperly? I'm confused as to why it works fine without the redirect_uri, but the site can't load when I provide it.
Can someone point me in the right direction on how I am supposed to pass a redirect url for my app, into an oauth redirect?
Update
It seems that Microsoft does not allow you to register a redirect URL that is a App URL scheme. I dont know how to get this info back into my app then, other than just paying for a site I can point MSFT to, which would then do nothing but redirects into the app for me.
I had a similar OAuth problem and didn't want to mess around with a web server so instead what I did was kinda cheeky. Make a UIWebView and put the request on the web view. Then delegate it to yourself and pass the redirect URL to be http://localhost:8000 (it can be anything like that it really doesn't matter). Then inside the delegate do this:
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool
{
if let URL = request.URL?.absoluteString
{
// This should be the redirect URL that you pass it can be anything like local host mentioned above.
if URL.hasPrefix(redirectURL)
{
// Now you can simply do some string manipulation to pull out the relevant components.
// I'm not sure what sort of token or how you get it back but assuming the redirect URL is
// YourRedirectURL&code=ACCESS_TOKEN and you want access token heres how you would get it.
var code : String?
if let URLParams = request.URL?.query?.componentsSeparatedByString("&")
{
for param in URLParams
{
let keyValue = param.componentsSeparatedByString("=")
let key = keyValue.first
if key == "code"
{
code = keyValue.last
}
}
}
// Here if code != nil then it has the ACCESS_TOKEN and you are done! If its nil something went wrong.
return false // So that the webview doesnt redirect to the dummy URL you passed.
}
}
return true
}
This is sorta hacky but it works great and you don't need any server nor do you need a redirect URI on your app, its an awesome way to do it in my opinion. You could optimize this for swift 2 to reduce the indentation by using guards but I wrote this before it came out so...
Hope this helps!