Skip to content
This repository has been archived by the owner on Nov 16, 2020. It is now read-only.

Commit

Permalink
Merge pull request #1 from vapor/start
Browse files Browse the repository at this point in the history
🚀
  • Loading branch information
tanner0101 authored Feb 16, 2017
2 parents 903c5eb + bdad50d commit 470241a
Show file tree
Hide file tree
Showing 19 changed files with 488 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
Package.pins

13 changes: 13 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import PackageDescription

let package = Package(
name: "Auth",
targets: [
Target(name: "Authentication"),
Target(name: "Authorization", dependencies: ["Authentication"]),
],
dependencies: [
// Swift models, relationships, and querying for NoSQL and SQL databases.
.Package(url: "https://github.com/vapor/fluent.git", Version(2,0,0, prereleaseIdentifiers: ["alpha"]))
]
)
35 changes: 35 additions & 0 deletions Sources/Authentication/AuthenticationError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
public enum AuthenticationError: Swift.Error {
case invalidBasicAuthorization
case invalidBearerAuthorization
case noAuthorizationHeader
case notAuthenticated
case invalidCredentials
case unsupportedCredentials
case unspecified(Error)
}

extension AuthenticationError: CustomStringConvertible {
public var description: String {
let reason: String

switch self {
case .invalidBasicAuthorization:
reason = "Invalid Authorization Basic header"
case .invalidBearerAuthorization:
reason = "Invalid Authorization Bearer header"
case .noAuthorizationHeader:
reason = "No authorization header"
case .notAuthenticated:
reason = "Not authenticated"
case .invalidCredentials:
reason = "Invalid credentials"
case .unsupportedCredentials:
reason = "Unsupported credentials"
case .unspecified(let error):
reason = "\(error)"
}

return "Authentication error: \(reason)"
}
}

1 change: 1 addition & 0 deletions Sources/Authentication/Credentials/Authenticatable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public protocol Authenticatable { }
1 change: 1 addition & 0 deletions Sources/Authentication/Credentials/Credentials.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public protocol Crendentials { }
6 changes: 6 additions & 0 deletions Sources/Authentication/Credentials/Custom.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// MARK: Authenticatable

public protocol CustomAuthenticatable: Authenticatable {
/// Returns the user matching the custom credential.
static func authenticate(custom: Crendentials) throws -> Self
}
33 changes: 33 additions & 0 deletions Sources/Authentication/Credentials/Identifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// MARK: Data structure

import Node

public struct Identifier: Crendentials {
let id: Node

public init(id: Node) {
self.id = id
}
}

// MARK: Authenticatable

public protocol IdentifierAuthenticatable {
/// Return the user with the supplied id.
static func authenticate(_: Identifier) throws -> Self
}


// MARK: Entity conformance

import Fluent

extension IdentifierAuthenticatable where Self: Entity {
public static func authenticate(_ id: Identifier) throws -> Self {
guard let match = try Self.find(id.id) else {
throw AuthenticationError.invalidCredentials
}

return match
}
}
55 changes: 55 additions & 0 deletions Sources/Authentication/Credentials/Token.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// MARK: Data structure

public struct Token: Crendentials {
public let string: String

public init(string: String) {
self.string = string
}
}

// MARK: Authenticatable

public protocol TokenAuthenticatable: Authenticatable {
/// The token entity that contains a foreign key
/// pointer to the user table
associatedtype TokenType: TokenProtocol

/// Returns the user matching the supplied token.
static func authenticate(_: Token) throws -> Self
}

public protocol TokenProtocol {
static var tokenKey: String { get }
static func findUser<U: Entity>(for: Token) throws -> U
}

extension TokenProtocol {
public static var tokenKey: String {
return "token"
}
}

// MARK: Entity conformance

import Fluent

extension TokenAuthenticatable where Self: Entity {
public static func authenticate(_ token: Token) throws -> Self {
return try TokenType.findUser(for: token)
}
}

extension TokenProtocol where Self: Entity {
public static func findUser<U: Entity>(for token: Token) throws -> U {
guard let user = try U.query()
.join(self)
.filter(self, tokenKey, token.string)
.first()
else {
throw AuthenticationError.invalidCredentials
}

return user
}
}
99 changes: 99 additions & 0 deletions Sources/Authentication/Credentials/UsernamePassword.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// MARK: Data structure

public struct Password: Crendentials {
public let username: String
public let password: String
public let verifier: PasswordVerifier?

public init(username: String, password: String, verifier: PasswordVerifier? = nil) {
self.username = username
self.password = password
self.verifier = verifier
}
}

// MARK: Authenticatable

public protocol PasswordAuthenticatable: Authenticatable {
// MARK: Username / Password
/// Return the user matching the supplied
/// username and password
static func authenticate(_: Password) throws -> Self

/// The entity's hashed password used for
/// validating against Password credentials
/// with a PasswordVerifier
var hashedPassword: String? { get }

/// The key under which the user's username,
/// email, or other identifing value is stored.
static var usernameKey: String { get }

/// The key under which the user's password
/// is stored.
static var passwordKey: String { get }
}

extension PasswordAuthenticatable {
public static var usernameKey: String {
return "email"
}

public static var passwordKey: String {
return "password"
}

public var hashedPassword: String? {
return nil
}
}

public protocol PasswordVerifier {
func verify(password: String, matchesHash: String) throws -> Bool
}

// MARK: Entity conformance

import Fluent

extension PasswordAuthenticatable where Self: Entity {
public static func authenticate(_ creds: Password) throws -> Self {
let user: Self

if let verifier = creds.verifier {
guard let match = try Self
.query()
.filter(usernameKey, creds.username)
.first()
else {
throw AuthenticationError.invalidCredentials
}

guard let hash = match.hashedPassword else {
throw AuthenticationError.invalidCredentials
}

guard try verifier.verify(
password: creds.password,
matchesHash: hash
) else {
throw AuthenticationError.invalidCredentials
}

user = match
} else {
guard let match = try Self
.query()
.filter(usernameKey, creds.username)
.filter(passwordKey, creds.password)
.first()
else {
throw AuthenticationError.invalidCredentials
}

user = match
}

return user
}
}
7 changes: 7 additions & 0 deletions Sources/Authentication/Header/Authorization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public struct Authorization {
public let header: String

public init(header: String) {
self.header = header
}
}
22 changes: 22 additions & 0 deletions Sources/Authentication/Header/Basic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Core

extension Authorization {
public var basic: Password? {
guard let range = header.range(of: "Basic ") else {
return nil
}

let token = header.substring(from: range.upperBound)


let decodedToken = token.makeBytes().base64Decoded.string
guard let separatorRange = decodedToken.range(of: ":") else {
return nil
}

let apiKeyID = decodedToken.substring(to: separatorRange.lowerBound)
let apiKeySecret = decodedToken.substring(from: separatorRange.upperBound)

return Password(username: apiKeyID, password: apiKeySecret)
}
}
10 changes: 10 additions & 0 deletions Sources/Authentication/Header/Bearer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
extension Authorization {
public var bearer: Token? {
guard let range = header.range(of: "Bearer ") else {
return nil
}

let token = header.substring(from: range.upperBound)
return Token(string: token)
}
}
3 changes: 3 additions & 0 deletions Sources/Authentication/User/User.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Fluent

// public protocol User: Entity {}
86 changes: 86 additions & 0 deletions Sources/Authorization/Authorizable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import Fluent

public protocol Authorizable: Entity {

}

extension Authorizable {
public func isAuthorized<
PermissionType: Permission,
PivotType: PivotProtocol & Entity
>(
to permission: PermissionType,
withPivot: PivotType.Type = PivotType.self
) throws -> Bool
where
PivotType.Left == Self,
PivotType.Right == PermissionType
{
guard let permission = try PermissionType.query().filter("key", permission.key).first() else {
throw AuthorizationError.unknownPermission
}

return try PivotType.related(self, permission)
}


public func assertAuthorization<
PermissionType: Permission,
PivotType: PivotProtocol & Entity
>(
to permission: PermissionType,
withPivot pivot: PivotType.Type = PivotType.self
) throws
where
PivotType.Left == Self,
PivotType.Right == PermissionType
{
guard try isAuthorized(to: permission, withPivot: PivotType.self) else {
throw AuthorizationError.notAuthorized
}
}
}

extension Authorizable {
public func isAuthorized<
PermissionType: Permission,
MiddleType: Entity,
PivotType: PivotProtocol & Entity
>(
to permission: PermissionType,
_ item: MiddleType,
withPivot: PivotType.Type = PivotType.self
) throws -> Bool
where
PivotType.Left: PivotProtocol,
PivotType.Left.Left == Self,
PivotType.Left.Right == MiddleType,
PivotType.Right == PermissionType
{
guard let permission = try PermissionType.query().filter("key", permission.key).first() else {
throw AuthorizationError.unknownPermission
}

return try PivotType.related(left: self, middle: item, right: permission)
}

public func assertAuthorization<
PermissionType: Permission,
MiddleType: Entity,
PivotType: PivotProtocol & Entity
>(
to permission: PermissionType,
_ item: MiddleType,
withPivot pivot: PivotType.Type = PivotType.self
) throws
where
PivotType.Left: PivotProtocol,
PivotType.Left.Left == Self,
PivotType.Left.Right == MiddleType,
PivotType.Right == PermissionType
{
guard try isAuthorized(to: permission, item, withPivot: PivotType.self) else {
throw AuthorizationError.notAuthorized
}
}
}
Loading

0 comments on commit 470241a

Please sign in to comment.