Recently, I have had a slight excess of ✨✨ f r e e t i m e ✨✨ and I have some of it to explore various APIs. For a recent app, I ended up with OAuth to access GitHub's API, and learnt about AuthenticationServices. I came across it once more a couple of days after, to access Google's API's and the documentation was horrible, and Google kept asking me to use the GoogleSignIn pod. I ended up reusing Authentication Services, and wanted to document this, since it was a major pain.
Note: don't forget to import Authentication Services
Setup the url with the correct parameters. This is a major pain point, and the oath will not go through unless the parameters are perfect. I'll be using Github as the example.
let base = "https://github.com/login/oauth"let endpoint = "/authorize?client_id=\(API.clientID)&scope=user,repo&redirect_uri=\(API.redirectURI)"
The redirect uri must be exactly the same that was used while making the app. It also needs to be added to the URL Scheme on Xcode.
Initiate an authentication session on trigger
guard let url = URL(string: base + endpoint) else { return }session = ASWebAuthenticationSession(url: url, callbackURLScheme: API.callback) { url, error in//TODO: get the authorization code}
Once the session is set up, we need to set it's context provider to the presenting ViewController and start it.
session?.presentationContextProvider = selfsession?.start()
The addition of the ContextProvider delegate will require us to extend ViewController to conform.
extension ViewController: ASWebAuthenticationPresentationContextProviding {func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {return view.window!}}
The ASWebAuthentication session return a url with the authorization code and other parameters. This authorization code would need to be exchanged for an access token and a refresh token (depends on the API).
session = ASWebAuthenticationSession(url: url, callbackURLScheme: API.callback) { url, error inlet code = String(url?.absoluteString.split(separator: "=")[1] ?? "")API.shared.authenticate(code: code) { status inif status {print("Yay") }else {print("Oops")}}}
func authenticate(code: String, completion: @escaping(Bool)->()) {let base = "https://github.com/login/oauth"let endpoint = "/access_token?client_id=\(API.clientID)&redirect_uri=\(API.callback)&client_secret=\(API.clientSecret)&code=\(code)"let url = URL(string: base + endpoint)!let session = URLSession.sharedlet task = session.dataTask(with: url) { data, response, error inguard let data = data else { return }guard let str = String(data: data, encoding: .utf8) else { return }if (str.contains("error") == true) {completion(false)}else {let accessToken = str.split(separator: "=")[1].split(separator: "&")[0]completion(true)}}task.resume()}
The access token is used in further requests, mostly as an Authorization Header
"Authorization": "token \(API.token)"
Google's process was just harder cause of the lack of good auth documentation, but once figured out, it's pretty much the same.
func authorization(_ viewController: UIViewController) {let base = "https://accounts.google.com/o/oauth2/v2"let endpoint = "/auth?scope=openid%20email%20profile&response_type=code&state=security_token%3D138r5719ru3e1%26url%3Dhttps%3A%2F%2Foauth2.example.com%2Ftoken&redirect_uri=\(API.redirectURI)://&client_id=\(YTAPI.clientID)"let session = ASWebAuthenticationSession(url: URL(string: base+endpoint)!, callbackURLScheme:\(API.redirectURI)) { url, error inif error == nil {guard let url = url else {return}let code = String(url.absoluteString.components(separatedBy: "&code=")[1].components(separatedBy: "&scope=")[0])self.tokens(code: code)}else {self.delegate?.didFail(with: error!)}}session.presentationContextProvider = (viewController as! ASWebAuthenticationPresentationContextProviding)session.start()}
And the tokens function would switch out the authorization code for access and refresh tokens.
func tokens(code: String) {let base = "https://oauth2.googleapis.com"let endpoint = "/token"let body = "code=\(code)&client_id=\(YTAPI.clientID)&redirect_uri=\(API.redirectURI)://&grant_type=authorization_code"request(type: .post, base: base, endpoint: endpoint, body: body) { (result: Result<User, Error>) inswitch result {case .success(let user):self.delegate?.didSignIn(with: user)case .failure(let error):self.delegate?.didFail(with: error)}}}
Since Google's Auth Tokens have an expiration code (unlike GitHub's), these would need to be refreshed periodically.
func refresh(token: String? = nil) {let base = "https://oauth2.googleapis.com"let endpoint = "/token"let body = "client_id=\(YTAPI.clientID)&refresh_token=\(token)&grant_type=refresh_token"request(type: .post, base: base, endpoint: endpoint, body: body) { (result: Result<Refresh, Error>) inswitch result {case .success(let refreshed):self.delegate?.didSignIn(with: globalUser)case .failure(let error):self.delegate?.didFail(with: error)}}}
All-in-all, I feel pretty confident to tackle future OAuth apps, and this serves an a lightweight option to using pods (pods = 🤮). I hope this helps you a little, and hopefully you can figure out your problem too.
You can find the code for GitHub in the source for my app, Feeder, which lets you watch your Github feed anywhere!
Please buy me a coffee if you like my work.