- Set up your application to use the OAuth2 protocol to access a user's GitHub account
GitHub's developer site provides a very succinct description of OAuth and why to use it.
OAuth2 is a protocol that lets external applications request authorization to private details in a user's GitHub account without getting their password. This is preferred over Basic Authentication because tokens can be limited to specific types of data, and can be revoked by users at any time.
IMPORTANT: Use GitHub's OAuth API documentation throughout this lab to understand the details of constructing and handling the requests and responses necessary for completing the process. Implementing the protocol requires joint effort from the application (client), the user (resource owner), and the website (resource server). Here is a brief summary of steps to use GitHub OAuth:
- Register your application with GitHub to receive a Client ID and a Client Secret.
- Set up an Authorization callback URL on GitHub.
- Set up a URL Scheme in Xcode for your application.
- Direct user from within your application to GitHub for authorization.
- Handle callback from GitHub containing a temporary code.
- Use code to authenticate user and receive access token.
- Save the access token in your application.
- Use the access token to make requests for user account information.
So why use OAuth in your application? Since your application will use resources from your user's GitHub account, following the OAuth protocol grants you access to those resources. Additionally, the user will not have to be authenticated by GitHub AND your application. This saves your poor user from the agony of remembering another username and password.
This project is similar to the previous GitHub related labs, however it has been updated and organized a bit differently. Here's a run down of what's right out of the box:
- Model
Repository
class is used to create repo objects.RepositoryDataStore
class storesRepository
objects.- View
RepositoryTableViewCell
class is the reusable cell for theRepositoryTableViewController
. This class also handles starring requests.- Controllers
AppController
class handles which view controller is displayed.LoginViewController
class directs the user for authorization and authentication.RepositoryTableViewController
class displays repositories and facilitates starring a repository.- Networking
GitHubAPIClient
class interacts with the GitHub API.- Utility
Constants
contains a struct of static storyboard IDs.Extensions
contains aNSURL
extension for parsing query items and aNotification.Name
extension of static notification names.Secrets
(you will need to add this file).- Pods
Locksmith
is a protocol-oriented library for working the keychain (e.g., save user's access token).- Etc.
For now, you can run the application to see some animated octocats. They are cheering for you so let's get started!
- Head on over to GitHub. Reference this screenshot to help you along the way.
- If you don't have a Client ID and a Client Secret set up from previous labs, go to Settings > OAuth Applications > Developer Applications > Register and start registering your new application.
- Whether you are registering a new application or have your application selected, find the header at the bottom of the form titled Authoriation callback URL.
- Enter some text following this format:
gitHubOAuthLab-12345://callback
. The first section before the colon,gitHubOAuthLab-12345
, can be whatever you want (e.g., "gitHubOAuthSuperDuperLab"). It's intended to be unique to your application. - Head on over to your project in Xcode and select your project in the the Project Navigator. Reference this screenshot to help you along the way.
- In the editor, select your project target, then select Info and look at the bottom of the list for URL Types.
- Expand the URL Types section and click on the plus sign.
- Enter your URL Scheme using the unique name you created above (e.g.,
gitHubOAuthLab-12345
) and press enter. This will update yourInfo.plist
file with your new URL scheme.
- Create your Secrets file and add your Client ID and Client Secret
struct Secrets {
static let clientID = ""
static let clientSecret = ""
}
The organization of the GitHubAPIClient
file is different from previous GitHub related labs. The GitHubRequestType
enum has associated types that provide the URL and HTTP Method for each type of request. The nested enums inside GitHubRequestType
and the buildParams(with:)
function are used to construct the different URL components of each type of request.
- Begin by adding the
oauth
case to theGitHubRequestType
enum. - The
oauth
case is used to redirect users to request GitHub access. - Add a static constant to
BaseURL
calledstandard
with a value of"https://github.com"
. - Add a static constant to
Path
calledoauth
with a value of"/login/oauth/authorize"
. - Add a static constant to
Query
calledoauth
. Refer to the other queries already listed to understand how they are constructed. The string query should be constructed with the following parameters: "client_id"
:Secrets.clientID
"scope"
:"repo"
- Add the
oauth
case to themethod
computed property andreturn nil
within theoauth
case. - Add the
oauth
case to theurl
computed property and return the complete URL. The complete URL should be the following,BaseURL.standard + Path.oauth + Query.oauth
.
You might see that you have two errors in Xcode which state that there are other switch statements that are not exhaustive. For those switch statements,--feel free to add in default implementation or just mix in the .oauth
case with another case for now as we'll be implementing this later.
- Locate the
loginButtonTapped(_:)
IBAction method in theLoginViewController
class. - Inside the method, pass the value from
GitHubRequestType.oauth.url
to to initialize aSFSafariViewController
. - Note: The safari view controller streamlines the process of directing a user to GitHub by providing easy access to a stripped down version of the Safari web browser inside your application.
- Hint: Import the Safari Services framework to use
SFSafariViewController
. Also, you will need to reference the safari view controller from a couple of methods within theLoginViewController
class. - Present the controller.
- Run the application to see if your safari view controller is presented when the login button is tapped. It should direct the user to the GitHub site to enter login information (Don't bother entering your GitHub credentials yet).
- Stop the application.
In the previous step the user is directed to GitHub using a safari view controller to provide authorization. Once the user successfully completes authorization, the callback you provided in your GitHub account is used to trigger the URL Scheme you provided in your project settings. Additionally, the safari view controller calls a UIApplicatioDelegate
method called application(_:open:options:)
that passes a URL containing a temporary code received from the GitHub callback.
- Add the
application(_:open:options:)
method to yourAppDelegate
file. This method looks like the following:
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
}
- Get the value for the key
UIApplicationOpenURLOptionsKey.sourceApplication
from the options dictionary argument. - If the value equals
"com.apple.SafariViewService"
, returntrue
. - Hint: The value from the options dictionary is of type
Any
and needs to be aString
in order to make the comparison.
Up until now you probably haven't used NotificationCenter
but you're about to take a crash course. In the simplest terms, you can post a notification saying, "HEY! SOMETHING HAPPENED!" An observer of the notification will be notified somewhere else in the application (and would probably say to themselves, "Why are you yelling at me? 😥").
Here are the two notification statements you will use in your application:
// Post notification
NotificationCenter.default.post(name:object:)
// Add observer
NotificationCenter.default.addObserver(_:selector:name:object:)
Now that you are a notification's expert, let's continue.
- In the previous step you verified the value,
"com.apple.SafariViewService"
and returnedtrue
. Add a post notification immediately before youreturn true
. Use theNotification.Name
extension from yourExtensions
file to provide the name.closeSafariVC
. Pass the value from the incomingurl
argument to theobject
parameter of the notification. This notification is posting a message to anyone listening that it's ok to close theSFSafariViewController
you presented inside theLoginViewController
. The notification also includes theurl
argument passed as an object. - Note: As mentioned above, the incoming
url
argument of theapplication(_:open:options:)
method contains a temporary code that we need to proceed with the GitHub authentication process. - Head back to the
LoginViewController
class and add a method calledsafariLogin(_:)
that takes one argument callednotification
of typeNotification
and returns nothing. - Add a notification observer inside
viewDidLoad()
of theLoginViewController
class. - The observer is the
LoginViewController
. - The selector is the method you just created above,
safariLogin(_:)
. - The name is the name you used for the post notification in the app delegate,
.closeSafariVC
. - The object is
nil
. - Inside
safariLogin(_:)
access the.object
property of thenotification
argument to get theURL
passed from the app delegate. - Hint: The
.object
property isAny?
. How can you turn that into aURL
. - Print the
URL
to the console. - Dismiss the safari view controller.
- Run the application, provide your credentials to GitHub in the safari view controller, and authorize the application.
- The URL containing the temporary code should print to the debugger and the safari view controller should be dismissed.
Now that you have received a URL containing a temporary code from GitHub, you can make a request to receive an access token. Head back to the GitHubAPIClient
file to update the GitHubRequestType
enum.
- Begin by adding the
token
case to theGitHubRequestType
enum. - The
token
case is used to request an access token. - Update the
token
case to accept aURL
argument namedurl
as an associated type. - The token request should use the
standard
static constant from theBaseURL
enum. - Add a static constant to
Path
calledaccessToken
with a value of"/login/oauth/access_token"
. - Instead of adding parameters to the
Query
enum, you are going to update thebuildParams(with code: String)
function that returns a dictionary of parameters. When requesting the access token, you will need to provide your"client_id"
,"client_secret"
, and the temporary"code"
you received back from GitHub as parameters. Thecode
argument in the function is the temporary code that needs to be included. - Complete the function by returning the dictionary of parameters. The function should only return the dictionary for the
token
case. Otherwise, it should returnnil
. - Add the
token
case to themethod
computed variable andreturn "POST"
. - Add the
token
case to theurl
computed variable and return the complete URL. - The URL should contain the
standard
base url and theaccessToken
path. ThebuildParams(with:)
function will be used in the next step.
Before you move forward, take a moment to look through GitHubAPIClient
. Take note of each section and how the methods are organized.
// MARK: Response Typealias
- Type aliases are handy for giving alias names to existing types. In this lab it's used as a means to organize the response from all GitHub requests.
// MARK: Request
- Routes all GitHub related requests through one method using the
GitHubRequestType
. // MARK: Request Generation
- Returns a
URLRequest
based on the type of the request. // MARK: Session Generation
- Returns a default
URLSession
. // MARK: Response Generation
- Executes a data task using the request type, session, and request. Contains a completion handler call back to pass three different values (See the type aliases mentioned above).
// MARK: Response Processing
- Separate methods for handling each type of request.
// MARK: Token Handling
- Separate methods and a variable for handling the access token received from GitHub.
// MARK: Error Handling
- An error enum to use for various errors that could occur during the request process or the response handling.
With all of that in mind, start by updating generateURLRequest(_:)
.
- Add the
token
case. - Remember this case has an associated
URL
type. When thetoken
case of theGitHubRequestType
is used, the URL containing the temporary code is passed in. You should use the value of the associated type to extract the temporary code from the URL that's passed in. You can capture the value in the case declaration like this:case .token(url: let url)
. - Declare a string constant called
code
where the value is the return of aURL
extension in theExtensions
file calledgetQueryItemValue(named:)
. To get the code from the URL, pass in"code"
as thenamed:
argument, the function will find the query item for the key"code"
usingURLComponents
. The function returns the code as an optional string. - Declare a constant called
parameters
where the value is the return of thebuildParams(with:)
function. Thecode
constant you just created needs to be added to a dictionary of parameters. Remember that you updated thebuildParams(with:)
function in the previous step. Use this function to pass in the code string in order to build a completed parameters dictionary for the request. - Create a
URLRequest
calledrequest
and update the.httpMethod
string property of the request using the type argument (Use the previous cases as a reference). - Use the
addValue(_:forHTTPHeaderField:)
function onURLRequest
to add the two following values to the header: "application/json"
for header field,"Accept"
.- Included to indicate to GitHub how the response should be formatted.
"application/json"
for header field,"Content-Type"
.- Included to indicate to GitHub how the parameters are formatted in the request.
- Use the
.httpBody
data property ofURLRequest
to add yourparameters
dictionary. - The current format of
parameters
needs to be serialized usingJSONSerialization
before it's applied as the value to.httpBody
. Use the class function onJSONSerialization
calleddata(withJSONObject:options:)
where the object isparameters
and the options are an empty array. - After applying the serialized parameters to
.httpBody
return therequest
.return nil
if the serialization fails.
8. Update generateResponse(type:session:request:completionHandler:)
to handle the token
case of the GitHubRequestType
enum
- Add the
token
case to the switch statement. CallprocessToken(response:)
to process the response. Use the other cases as a reference.
- Update the
processToken(response:)
method to get the "access_token" from the JSON dictionary (use the incomingData?
argument to implement the JSON serialization). Once you get the access token from the JSON dictionary, save it as a string usingsaveAccess(token: String)
. The string is saved using theLocksmith
pod. If all goes well, yourResponse
return should be allnil
values, otherwise bubble up an error in your response. - HINT: If you're unsure of how to construct the method, use the other response processing methods as a reference.
Now that the GitHubAPIClient
file has been updated to handle the request for an access token, it's time to turn your attention back to the LoginViewController
. If you remember, the LoginViewController
presents a SFSafariViewController
to begin the authorization process with GitHub.
When GitHub redirects to your application with a temporary code, application(_:open:options:)
is called in the app delegate. This is where you posted a notification saying it's ok to close the safari view controller. The notification observer you added to the LoginViewController
calls safariLogin(_:)
. This is the method where you retrieved the URL from the notification object
property.
As mentioned previously, the URL containing the temporary code now needs to be passed in as a part of the request to the GitHubAPIClient
using the token
case of the GitHubRequestType
enum.
- Call the
request(_:completionHandler:)
fromGitHubAPIClient
using thetoken
request type. Pass the URL as the value of the associated type. - If error is
nil
, add the following statement:NotificationCenter.default.post(name: .closeLoginVC, object: nil)
. - NOTE: The completion handler has three arguments. For the token request, you are only concerned with whether or not an error has occurred. If there has not been an error, a notification will be posted to an observer in the
AppController
to close theLoginViewController
and present theRepositoryTableViewController
. - Build and run the application. If everything is set up correctly, you should see the
RepositoryTableViewController
displaying a list of repositories.
-
Starring
-
Update the
starred(token:)
static function of the privateQuery
enum insideGitHubRequestType
. The incoming token argument needs to be added to the return string in order for the application to star/unstar the repositories listed in theReposTableViewController
. Build and run the application to see if a star icon appears in each row next to the repository name. A user should be able to tap a star icon to star/unstar repositories. -
Logout
- The
RepositoryTableViewController
has an IBAction for the log out button. This method needs to calldeleteAccessToken()
fromGitHubAPIClient
and use the optionalError
return to determine whether to post a notification to theAppController
to close the table view controller.deleteAccessToken()
still needs to be defined. It should delete the token and return an error if one occurs.
- The