Skip to content

Commit 4280ebf

Browse files
maxalbrightmetameta-codesync[bot]
authored andcommitted
Add Permission Revocation Demo to FB Login Sample App
Summary: Just like we do for Android, we should have a demo in the FB Login Sample App of revoking access tokens. This will empower advertisers to make better security decisions. Reviewed By: mtopala00 Differential Revision: D90036836 fbshipit-source-id: 9be2a5a
1 parent 2f71614 commit 4280ebf

File tree

1 file changed

+172
-0
lines changed

1 file changed

+172
-0
lines changed

samples/FacebookLoginSample/FacebookLoginSample/LoginDetailsViewController.swift

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@ final class LoginDetailsViewController: UIViewController {
2424
private static let missingProfile = "Current Profile unavailable"
2525
private static let missingAccessToken = "Access Token Unavailable"
2626

27+
private var buttonContainerView: UIView!
28+
2729
override func viewDidLoad() {
2830
super.viewDidLoad()
2931

32+
setupButtonContainer()
33+
3034
let accessToken = AccessToken.current
3135
let authenticationToken = AuthenticationToken.current
3236
let profile = Profile.current
@@ -50,4 +54,172 @@ final class LoginDetailsViewController: UIViewController {
5054
emailLabel.text = profile?.email ?? "Email unavailable"
5155
}
5256

57+
private func setupButtonContainer() {
58+
buttonContainerView = UIView()
59+
if #available(iOS 13.0, *) {
60+
buttonContainerView.backgroundColor = .systemBackground
61+
} else {
62+
buttonContainerView.backgroundColor = .white
63+
}
64+
buttonContainerView.translatesAutoresizingMaskIntoConstraints = false
65+
view.addSubview(buttonContainerView)
66+
67+
let revokeButton = UIButton(type: .system)
68+
revokeButton.setTitle("Revoke Access Token", for: .normal)
69+
revokeButton.translatesAutoresizingMaskIntoConstraints = false
70+
revokeButton.addTarget(self, action: #selector(revokeAccessTokenTapped), for: .touchUpInside)
71+
buttonContainerView.addSubview(revokeButton)
72+
73+
let refreshButton = UIButton(type: .system)
74+
refreshButton.setTitle("Refresh Token", for: .normal)
75+
refreshButton.translatesAutoresizingMaskIntoConstraints = false
76+
refreshButton.addTarget(self, action: #selector(refreshTokenTapped), for: .touchUpInside)
77+
buttonContainerView.addSubview(refreshButton)
78+
79+
NSLayoutConstraint.activate([
80+
buttonContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
81+
buttonContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
82+
buttonContainerView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
83+
84+
revokeButton.topAnchor.constraint(equalTo: buttonContainerView.topAnchor, constant: 12),
85+
revokeButton.centerXAnchor.constraint(equalTo: buttonContainerView.centerXAnchor),
86+
87+
refreshButton.topAnchor.constraint(equalTo: revokeButton.bottomAnchor, constant: 12),
88+
refreshButton.centerXAnchor.constraint(equalTo: buttonContainerView.centerXAnchor),
89+
refreshButton.bottomAnchor.constraint(equalTo: buttonContainerView.bottomAnchor, constant: -12),
90+
])
91+
}
92+
93+
@objc private func revokeAccessTokenTapped() {
94+
guard AccessToken.current != nil else {
95+
showAlert(title: "Error", message: "No access token available. Please log in first.")
96+
return
97+
}
98+
executeRevokeRequest()
99+
}
100+
101+
@objc private func refreshTokenTapped() {
102+
refreshAccessToken()
103+
}
104+
105+
private func executeRevokeRequest() {
106+
guard let accessToken = AccessToken.current?.tokenString else {
107+
showAlert(title: "Error", message: "No access token available")
108+
return
109+
}
110+
111+
let urlString = "https://graph.facebook.com/v17.0/me/permissions?access_token=\(accessToken)"
112+
guard let url = URL(string: urlString) else {
113+
showAlert(title: "Error", message: "Invalid URL")
114+
return
115+
}
116+
117+
var request = URLRequest(url: url)
118+
request.httpMethod = "DELETE"
119+
120+
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
121+
DispatchQueue.main.async {
122+
if let error = error as NSError? {
123+
self?.showAlert(
124+
title: "Error (\(error.code))",
125+
message: "\(error.localizedDescription)"
126+
)
127+
return
128+
}
129+
130+
self?.showAlert(title: "Token Revoked", message: "All permissions have been revoked. Tap 'Refresh Token' to update the UI.")
131+
}
132+
}
133+
task.resume()
134+
}
135+
136+
private func showAlert(title: String, message: String) {
137+
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
138+
alert.addAction(UIAlertAction(title: "OK", style: .default))
139+
present(alert, animated: true)
140+
}
141+
142+
private func refreshAccessToken() {
143+
guard let accessToken = AccessToken.current?.tokenString else {
144+
showAlert(title: "Error", message: "No access token available")
145+
return
146+
}
147+
148+
let urlString = "https://graph.facebook.com/v17.0/me?fields=id,name,email&access_token=\(accessToken)"
149+
guard let url = URL(string: urlString) else {
150+
showAlert(title: "Error", message: "Invalid URL")
151+
return
152+
}
153+
154+
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
155+
DispatchQueue.main.async {
156+
if let error = error {
157+
self?.showAlert(title: "Network Error", message: error.localizedDescription)
158+
return
159+
}
160+
161+
guard let data = data else {
162+
self?.showAlert(title: "Error", message: "No data received")
163+
return
164+
}
165+
166+
do {
167+
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
168+
if let errorInfo = json["error"] as? [String: Any] {
169+
// Token is invalid/revoked
170+
let message = errorInfo["message"] as? String ?? "Token is invalid"
171+
AccessToken.current = nil
172+
AuthenticationToken.current = nil
173+
Profile.current = nil
174+
self?.updateUIAfterRevocation()
175+
self?.showAlert(title: "Token Invalid", message: message)
176+
} else {
177+
// Token is valid
178+
self?.updateUIWithCurrentToken()
179+
let name = json["name"] as? String ?? "Unknown"
180+
self?.showAlert(title: "Token Valid", message: "Token is valid for user: \(name)")
181+
}
182+
}
183+
} catch {
184+
self?.showAlert(title: "Parse Error", message: error.localizedDescription)
185+
}
186+
}
187+
}
188+
task.resume()
189+
}
190+
191+
private func updateUIAfterRevocation() {
192+
accessTokenLabel.text = "REVOKED"
193+
permissionsLabel.text = "REVOKED"
194+
declinedPermissionsLabel.text = "REVOKED"
195+
authenticationTokenLabel.text = "REVOKED"
196+
nonceLabel.text = "REVOKED"
197+
nameLabel.text = Self.missingProfile
198+
userIdentifierLabel.text = Self.missingProfile
199+
emailLabel.text = "Email unavailable"
200+
}
201+
202+
private func updateUIWithCurrentToken() {
203+
let accessToken = AccessToken.current
204+
let authenticationToken = AuthenticationToken.current
205+
let profile = Profile.current
206+
207+
accessTokenLabel.text = accessToken?.tokenString ?? Self.missingAccessToken
208+
permissionsLabel.text = accessToken?.permissions
209+
.map { $0.name }
210+
.joined(separator: ", ")
211+
?? Self.missingAccessToken
212+
declinedPermissionsLabel.text = accessToken?.declinedPermissions
213+
.map { $0.name }
214+
.joined(separator: ", ")
215+
?? Self.missingAccessToken
216+
authenticationTokenLabel.text = authenticationToken?.tokenString
217+
?? "Authentication Token Unavailable"
218+
nonceLabel.text = authenticationToken?.nonce
219+
?? "Authentication Token Nonce Unavailable"
220+
nameLabel.text = profile?.name ?? Self.missingProfile
221+
userIdentifierLabel.text = profile?.userID ?? Self.missingProfile
222+
emailLabel.text = profile?.email ?? "Email unavailable"
223+
}
224+
53225
}

0 commit comments

Comments
 (0)