diff --git a/README.md b/README.md index 76cec32..2589255 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,111 @@ Authentication framework for Go applications. ## Features - * OAuth2 - * Multiple providers + * OAuth2 today + * Supports other protocols too + * [Multiple providers](https://github.com/stretchr/gomniauth/tree/master/providers) * Comes with Google and GitHub baked in * Easily extensible - * Example web app to copy + * [Example web app](https://github.com/stretchr/gomniauth/tree/master/example) to copy + * Works beautifully with [Goweb](https://github.com/stretchr/goweb) + * Fully [TDD](http://en.wikipedia.org/wiki/Test-driven_development) + +## Documentation + + * Jump right into the [API Documentation](http://godoc.org/github.com/stretchr/gomniauth) + +## Get started + +Install Gomniauth by doing: + + go get github.com/stretchr/gomniauth + +Check out the [example web app code](https://github.com/stretchr/gomniauth/tree/master/example) to see how to use Gomniauth using [Goweb](https://github.com/stretchr/goweb). + +## Contributing + + * If you add a provider that others could also make use of, please send us a Pull Request and we'll add it to the repo. + +## Implementing Gomniauth + +### Set up Gomniauth + +First and only once for your application, you need to setup the security key and providers. The security key is used when hashing any data that gets transmitted to ensure it's integrity. + +You are free to use the [signature package's RandomKey function](http://godoc.org/github.com/stretchr/signature#RandomKey) to generate a unique code every time your application starts. + + gomniauth.SetSecurityKey(signature.RandomKey(64)) + +A provider represents an authentication service that will be available to your users. Usually, you'll have to add some configuration of your own, such as your application `key` and `secret` (provided by the auth service), and the `callback` into your app where users will be sent following successful (or not) authentication. + + gomniauth.WithProviders( + github.New("key", "secret", "callback"), + google.New("key", "secret", "callback"), + ) + +#### What kind of callback? + +The callback should be an absolute URL to your application and should include the provider name in some way. + +For example, in the [example web app](https://github.com/stretchr/gomniauth/tree/master/example) we used the following format for callbacks: + + http://mydomain.com/auth/{provider}/callback + +### Are they logged in? + +When a user tries to access a protected resource, or if you want to make third party authenticated API calls, you need a mechanism to decide whether a user is logged in or not. For web apps, cookies usually work, if you're building an API, then you should consider some kind of auth token. + +### Decide how to log in + +If they are not logged in, you need to provide them with a list of providers from which they can choose. + +You can access a list of the providers you are supporting by calling the `gomniauth.Providers()` function. + +### Redirecting them to the login page + +Once a provider has been chosen, you must redirect them to be authenticated. You can do this by using the `gomniauth.Provider` function, that will return a [Provider](http://godoc.org/github.com/stretchr/gomniauth/common#Provider) by name. + +So if the user chooses to login using Github, you would do: + + provider, err := gomniauth.Provider("github") + +Once you have your provider, you can get the URL to redirect users to by calling: + + authUrl, err := provider.GetBeginAuthURL(state, options) + +You should then redirect the user to the `authUrl`. + +#### State and options + +The `state` parameter is a `State` object that contains information that will be hashed and passed (via the third party) through to your callback (see below). Usually, this object contains the URL to redirect to once authentication has completed, but you can store whatever you like in here. + +The `options` parameter is an `objects.Map` containing additional query-string parameters that will be sent to the authentication service. For example, in OAuth2 implementations, you can specify a `scope` parameter to get additional access to other services you might need. + +### Handling the callback + +Once the third party authentication service has finished processing the request, they will send the user back to your callback URL. + +Remember, you specified the callback URL when you setup your providers. + +For example, the user might hit: + + http://yourdomain.com/auth/github/callback?code=abc123 + +You don't need to worry about the detail of the parameters passed back, because Gomniauth takes care of those for you. You just need to pass them into the `CompleteAuth` method of your provider: + + provider, err := gomniauth.Provider("github") + creds, err := provider.CompleteAuth(queryParams) + +NOTE: It's unlikely you'll hard-code the provider name, we have done it here to make it obvious what's going on. + +The provider will then do the work in the background to complete the authentication and return the `creds` for that user. The `creds` are then used to make authenticated requests to the third party (in this case Github) on behalf of the user. + +### Getting the user information + +If you then want some information about the user who just authenticated, you can call the `GetUser` method on the provider (passing in the `creds` from the `CompleteAuth` method.) + +The [User](https://github.com/stretchr/gomniauth/blob/master/common/user.go) you get back will give you access to the common user data you will need (like name, email, avatar URL etc) and also an `objects.Map` of `Data()` that contains everything else. + +### Caching in + +Once you had the credentials for a user for a given provider, you should cache them in your own datastore. This will mean that if the cookie hasn't expired, or if the client has stored the auth token, they can continue to use the service without having to log in again. \ No newline at end of file diff --git a/common/credentials_test.go b/common/credentials_test.go index 0070385..1d7bcc9 100644 --- a/common/credentials_test.go +++ b/common/credentials_test.go @@ -2,6 +2,7 @@ package common import ( "github.com/stretchr/codecs" + "github.com/stretchr/core/stretchr/constants" "github.com/stretchr/stew/objects" "github.com/stretchr/testify/assert" "testing" @@ -9,12 +10,12 @@ import ( func TestCredentials_PublicData(t *testing.T) { - creds := &Credentials{objects.M("authcode", "ABC123", CredentialsKeyID, 123)} + creds := &Credentials{objects.M(constants.ParamAuthCode, "ABC123", CredentialsKeyID, 123)} publicData, _ := codecs.PublicData(creds, nil) if assert.NotNil(t, publicData) { - assert.Equal(t, "ABC123", publicData.(objects.Map)["authcode"]) + assert.Equal(t, "ABC123", publicData.(objects.Map)[constants.ParamAuthCode]) assert.Equal(t, "123", publicData.(objects.Map)[CredentialsKeyID], "CredentialsKeyID ("+CredentialsKeyID+") must be turned into a string.") } diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..041886e --- /dev/null +++ b/doc.go @@ -0,0 +1,5 @@ +// General use authentication package for Go. +// +// To get started, visit the Gomniauth GitHub project +// homepage: https://github.com/stretchr/gomniauth +package gomniauth diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..11d8a42 --- /dev/null +++ b/example/README.md @@ -0,0 +1,9 @@ +## Examples + +### Goweb example app + +The [main.go](https://github.com/stretchr/gomniauth/blob/master/example/goweb/main.go) file is an example of how to implement Gomniauth with [Goweb](http://github.com/stretchr/goweb). + +### Other examples + +Please feel free to add your own examples and send us a pull request. diff --git a/example/goweb/README.md b/example/goweb/README.md new file mode 100644 index 0000000..2812a2d --- /dev/null +++ b/example/goweb/README.md @@ -0,0 +1,3 @@ +### Goweb example app + +The [main.go](https://github.com/stretchr/gomniauth/blob/master/example/goweb/main.go) file is an example of how to implement Gomniauth with [Goweb](http://github.com/stretchr/goweb). \ No newline at end of file diff --git a/example/goweb/main.go b/example/goweb/main.go new file mode 100644 index 0000000..2649d1a --- /dev/null +++ b/example/goweb/main.go @@ -0,0 +1,184 @@ +package main + +import ( + "fmt" + "github.com/stretchr/gomniauth" + "github.com/stretchr/gomniauth/providers/github" + "github.com/stretchr/gomniauth/providers/google" + "github.com/stretchr/goweb" + "github.com/stretchr/goweb/context" + "log" + "net" + "net/http" + "os" + "os/signal" + "time" +) + +const ( + // NOTE: Don't change this, the auth settings on the providers + // are coded to this path for this example. + Address string = ":8080" +) + +func write(ctx context.Context, output string) { + ctx.HttpResponseWriter().Write([]byte(output)) +} + +func writeHeader(ctx context.Context) { + write(ctx, "Gomniauth - Example web app") +} + +func respondWithError(ctx context.Context, errorMessage string) error { + writeHeader(ctx) + write(ctx, fmt.Sprintf("Error: %s", errorMessage)) + return nil +} + +func main() { + + // setup the providers + gomniauth.SetSecurityKey("yLiCQYG7CAflDavqGH461IO0MHp7TEbpg6TwHBWdJzNwYod1i5ZTbrIF5bEoO3oP") // NOTE: DO NOT COPY THIS - MAKE YOR OWN! + gomniauth.WithProviders( + github.New("3d1e6ba69036e0624b61", "7e8938928d802e7582908a5eadaaaf22d64babf1", "http://localhost:8080/auth/github/callback"), + google.New("1051709296778.apps.googleusercontent.com", "7oZxBGwpCI3UgFMgCq80Kx94", "http://localhost:8080/auth/google/callback"), + ) + + /* + GET /auth/{provider}/login + + Redirects them to the fmtin page for the specified provider. + */ + goweb.Map("auth/{provider}/login", func(ctx context.Context) error { + + provider, err := gomniauth.Provider(ctx.PathValue("provider")) + + if err != nil { + return err + } + + state := gomniauth.NewState("after", "success") + + // if you want to request additional scopes from the provider, + // pass them as login?scope=scope1,scope2 + //options := objects.M("scope", ctx.QueryValue("scope")) + + authUrl, err := provider.GetBeginAuthURL(state, nil) + + if err != nil { + return err + } + + // redirect + return goweb.Respond.WithRedirect(ctx, authUrl) + + }) + + goweb.Map("auth/{provider}/callback", func(ctx context.Context) error { + + provider, err := gomniauth.Provider(ctx.PathValue("provider")) + + if err != nil { + return err + } + + creds, err := provider.CompleteAuth(ctx.QueryParams()) + + if err != nil { + return err + } + + /* + // get the state + state, stateErr := gomniauth.StateFromParam(ctx.QueryValue("state")) + + if stateErr != nil { + return stateErr + } + + // redirect to the 'after' URL + afterUrl := state.GetStringOrDefault("after", "error?e=No after parameter was set in the state") + + */ + + // load the user + user, userErr := provider.GetUser(creds) + + if userErr != nil { + return userErr + } + + return goweb.API.RespondWithData(ctx, user) + + // redirect + //return goweb.Respond.WithRedirect(ctx, afterUrl) + + }) + + /* + ---------------------------------------------------------------- + START OF WEB SERVER CODE + ---------------------------------------------------------------- + */ + + log.Println("Starting...") + fmt.Print("Gomniauth - Example web app\n") + fmt.Print("by Mat Ryer and Tyler Bunnell\n") + fmt.Print(" \n") + fmt.Print("Starting Goweb powered server...\n") + + // make a http server using the goweb.DefaultHttpHandler() + s := &http.Server{ + Addr: Address, + Handler: goweb.DefaultHttpHandler(), + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + listener, listenErr := net.Listen("tcp", Address) + + fmt.Printf(" visit: %s\n", Address) + + if listenErr != nil { + log.Fatalf("Could not listen: %s", listenErr) + } + + fmt.Println("\n") + fmt.Println("Try some of these routes:\n") + fmt.Printf("%s", goweb.DefaultHttpHandler()) + fmt.Println("\n\n") + + go func() { + for _ = range c { + + // sig is a ^C, handle it + + // stop the HTTP server + fmt.Print("Stopping the server...\n") + listener.Close() + + /* + Tidy up and tear down + */ + fmt.Print("Tearing down...\n") + + // TODO: tidy code up here + + log.Fatal("Finished - bye bye. ;-)\n") + + } + }() + + // begin the server + log.Fatalf("Error in Serve: %s\n", s.Serve(listener)) + + /* + ---------------------------------------------------------------- + END OF WEB SERVER CODE + ---------------------------------------------------------------- + */ + +} diff --git a/example/goweb/main_test.go b/example/goweb/main_test.go new file mode 100644 index 0000000..b80884e --- /dev/null +++ b/example/goweb/main_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "testing" +) + +// TestSomething is a place holder to make sure this package +// gets built / compiled when running tests. +// +// See the notes in main.go to see the example in action. +func TestSomething(t *testing.T) {} diff --git a/example/main.go b/example/main.go index 40389e7..aac4072 100644 --- a/example/main.go +++ b/example/main.go @@ -1,184 +1,7 @@ package main -import ( - "fmt" - "github.com/stretchr/gomniauth" - "github.com/stretchr/gomniauth/providers/github" - "github.com/stretchr/gomniauth/providers/google" - "github.com/stretchr/goweb" - "github.com/stretchr/goweb/context" - "log" - "net" - "net/http" - "os" - "os/signal" - "time" -) +/* -const ( - // NOTE: Don't change this, the auth settings on the providers - // are coded to this path for this example. - Address string = ":8080" -) + This has moved to https://github.com/stretchr/gomniauth/tree/master/example/goweb -func write(ctx context.Context, output string) { - ctx.HttpResponseWriter().Write([]byte(output)) -} - -func writeHeader(ctx context.Context) { - write(ctx, "Gomniauth - Example web app") -} - -func respondWithError(ctx context.Context, errorMessage string) error { - writeHeader(ctx) - write(ctx, fmt.Sprintf("Error: %s", errorMessage)) - return nil -} - -func main() { - - // setup the providers - gomniauth.SetSecurityKey("yLiCQYG7CAflDavqGH461IO0MHp7TEbpg6TwHBWdJzNwYod1i5ZTbrIF5bEoO3oP") // NOTE: DO NOT COPY THIS - MAKE YOR OWN! - gomniauth.WithProviders( - github.New("3d1e6ba69036e0624b61", "7e8938928d802e7582908a5eadaaaf22d64babf1", "http://localhost:8080/auth/github/callback"), - google.New("1051709296778.apps.googleusercontent.com", "7oZxBGwpCI3UgFMgCq80Kx94", "http://localhost:8080/auth/google/callback"), - ) - - /* - GET /auth/{provider}/login - - Redirects them to the fmtin page for the specified provider. - */ - goweb.Map("auth/{provider}/login", func(ctx context.Context) error { - - provider, err := gomniauth.Provider(ctx.PathValue("provider")) - - if err != nil { - return err - } - - state := gomniauth.NewState("after", "success") - - // if you want to request additional scopes from the provider, - // pass them as login?scope=scope1,scope2 - //options := objects.M("scope", ctx.QueryValue("scope")) - - authUrl, err := provider.GetBeginAuthURL(state, nil) - - if err != nil { - return err - } - - // redirect - return goweb.Respond.WithRedirect(ctx, authUrl) - - }) - - goweb.Map("auth/{provider}/callback", func(ctx context.Context) error { - - provider, err := gomniauth.Provider(ctx.PathValue("provider")) - - if err != nil { - return err - } - - creds, err := provider.CompleteAuth(ctx.QueryParams()) - - if err != nil { - return err - } - - /* - // get the state - state, stateErr := gomniauth.StateFromParam(ctx.QueryValue("state")) - - if stateErr != nil { - return stateErr - } - - // redirect to the 'after' URL - afterUrl := state.GetStringOrDefault("after", "error?e=No after parameter was set in the state") - - */ - - // load the user - user, userErr := provider.GetUser(creds) - - if userErr != nil { - return userErr - } - - return goweb.API.RespondWithData(ctx, user) - - // redirect - //return goweb.Respond.WithRedirect(ctx, afterUrl) - - }) - - /* - ---------------------------------------------------------------- - START OF WEB SERVER CODE - ---------------------------------------------------------------- - */ - - log.Println("Starting...") - fmt.Print("Gomniauth - Example web app\n") - fmt.Print("by Mat Ryer and Tyler Bunnell\n") - fmt.Print(" \n") - fmt.Print("Starting Goweb powered server...\n") - - // make a http server using the goweb.DefaultHttpHandler() - s := &http.Server{ - Addr: Address, - Handler: goweb.DefaultHttpHandler(), - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - MaxHeaderBytes: 1 << 20, - } - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - listener, listenErr := net.Listen("tcp", Address) - - fmt.Printf(" visit: %s\n", Address) - - if listenErr != nil { - log.Fatalf("Could not listen: %s", listenErr) - } - - fmt.Printf("\n") - fmt.Printf("Try some of these routes:\n") - fmt.Printf("%s", goweb.DefaultHttpHandler()) - fmt.Printf("\n\n") - - go func() { - for _ = range c { - - // sig is a ^C, handle it - - // stop the HTTP server - fmt.Print("Stopping the server...\n") - listener.Close() - - /* - Tidy up and tear down - */ - fmt.Print("Tearing down...\n") - - // TODO: tidy code up here - - log.Fatal("Finished - bye bye. ;-)\n") - - } - }() - - // begin the server - log.Fatalf("Error in Serve: %s\n", s.Serve(listener)) - - /* - ---------------------------------------------------------------- - END OF WEB SERVER CODE - ---------------------------------------------------------------- - */ - -} +*/ diff --git a/example/main_test.go b/example/main_test.go deleted file mode 100644 index aa3ae02..0000000 --- a/example/main_test.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import ( - "testing" -) - -func TestSomething(t *testing.T) {} diff --git a/oauth2/README.md b/oauth2/README.md new file mode 100644 index 0000000..5f5e182 --- /dev/null +++ b/oauth2/README.md @@ -0,0 +1,3 @@ +### OAuth2 package + +Inspired by [goauth2](https://code.google.com/p/goauth2) \ No newline at end of file