The app so far has the ability to read its configuration and to check that you are who you say you are on devices that support touch or face id.
The next step is to get an access token from the S'banken API.
Tokens
S'banken is using a standard authentication mechanism - OAuth client credentials.
To make this call we need to send a POST request to the token endpoint with the following headers:
The body must be
grant_type=client_credentials
So - that authorization header. This call uses basic auth - which is a base 64 encoded version of username:password
. Here - username is the clientId and password is the clientSecret. But we need to do one more step - url encode each part before we base 64 encode it.
Let's create a little utility class with a static function. It will take a config object and a completion callback (the request will be async).
Add a new file - TokenService.swift. At the top - add an import for Alamofire
We'll start by creating a Codable representation of the response:
struct Token : Decodable {
let accessToken: String
let expiresIn: Int
let tokenType: String
enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case expiresIn = "expires_in"
case tokenType = "token_type"
}
}
This will be used when parsing the response from the API.
Percent Encoding
Now - encoding the username and password fields. Swift strings have a method addingPercentEncoding which will do this for us - given the correct allowed CharacterSet. I found that none of the default available character sets worked as I wanted so I took a peek at S'banken's own swift API implementation: https://github.com/Sbanken/sbankenclient-ios/blob/master/SbankenClient/SbankenClient.swift#L257-L261 and found that they are constructing a character set - so - let's do that.
We'll add it as an extension onto String:
extension String {
public func encodeForAuth() -> String? {
let characterSet = NSMutableCharacterSet.alphanumeric()
characterSet.addCharacters(in: "-_.!~*'()")
return self.addingPercentEncoding(withAllowedCharacters: characterSet as CharacterSet)
}
}
Request
Finally we can start to construct our request.
We create a TokenService class with a static getToken function:
class TokenService {
public static func getToken(config: Config, onComplete: @escaping (_ accessToken: String?) -> Void) {
// Get username param encoded
guard let username = config.clientId.encodeForAuth() else {
onComplete(nil)
return
}
// Get password param encoded
guard let password = config.clientSecret.encodeForAuth() else {
onComplete(nil)
return
}
// Construct the basic auth string
let basicAuth = Data("\(username):\(password)".utf8).base64EncodedString()
// Headers required
let headers: HTTPHeaders = [
"Authorization": "Basic \(basicAuth)",
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded"
]
// Body params
let parameters = ["grant_type": "client_credentials"]
// Construct the request - setting the body to x-www-form-urlencoded
let request = AF.request("https://auth.sbanken.no/identityserver/connect/token",
method: .post,
parameters: parameters,
encoding: URLEncoding.httpBody,
headers: headers)
let decoder = JSONDecoder()
// Call the API
request.responseDecodable(of: Token.self, decoder: decoder) { (response) in
if let error = response.error {
// It went wrong
print("Unable to fetch token \(response) \(error.localizedDescription)")
onComplete(nil)
return
}
guard let token = response.value else {
// We got an answer but could not parse it
print("Unable to read token")
onComplete(nil)
return
}
// Token received
onComplete(token.accessToken)
}
}
}
Triggering the get token call
For now - in the main view - in the onAppear - we load config and ensure that you are logged in.
Add the following function to ContentView - it will call the service if the config is available and then print to console what we got back
func getToken() {
if let config = self.config {
TokenService.getToken(config: config) { (accessToken) in
print("\(accessToken ?? "No token")")
}
}
}
Finally - for testing - we can add self.getToken()
to the OK clause of askForAuth():
case .OK:
self.authenticated = true
self.getToken()
This will have to be updated later - but for now it will allow us to trigger the call for testing.
Summary
So - we now have a token. The next step will be to add support for fetching account info and transaction info.